diff --git a/packages/forms/docs/03-fields/03-select.md b/packages/forms/docs/03-fields/03-select.md index 7f5143b689..8973c2e295 100644 --- a/packages/forms/docs/03-fields/03-select.md +++ b/packages/forms/docs/03-fields/03-select.md @@ -337,19 +337,6 @@ MorphToSelect::make('commentable') > Many of the same options in the select field are available for `MorphToSelect`, including `searchable()`, `preload()`, `allowHtml()`, and `optionsLimit()`. -### Forcing case-insensitive search - -By default, searching will use the sensitivity settings from the database table column. This is to avoid unnecessary performance overhead when searching large datasets that would arise if we were to force insensitivity for all users. However, if your database does not search case-insensitively by default, you can force it to by using the `forceSearchCaseInsensitive()` method: - -```php -use Filament\Forms\Components\Select; - -Select::make('author_id') - ->relationship(name: 'author', titleAttribute: 'name') - ->searchable() - ->forceSearchCaseInsensitive() -``` - ## Allowing HTML in the option labels By default, Filament will escape any HTML in the option labels. If you'd like to allow HTML, you can use the `allowHtml()` method: diff --git a/packages/forms/src/Components/MorphToSelect/Type.php b/packages/forms/src/Components/MorphToSelect/Type.php index 91c24ccadd..b20325599a 100644 --- a/packages/forms/src/Components/MorphToSelect/Type.php +++ b/packages/forms/src/Components/MorphToSelect/Type.php @@ -6,6 +6,7 @@ use Exception; use Filament\Forms\Components\Select; use function Filament\Support\get_model_label; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Query\Expression; @@ -36,7 +37,7 @@ class Type protected string $model; - protected bool $isSearchForcedCaseInsensitive = false; + protected ?bool $isSearchForcedCaseInsensitive = null; final public function __construct(string $model) { @@ -65,10 +66,12 @@ protected function setUp(): void $query->orderBy($this->getTitleAttribute()); } - $search = Str::lower($search); - $isFirst = true; - $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive(); + $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive($query); + + if ($isForcedCaseInsensitive) { + $search = Str::lower($search); + } $query->where(function (Builder $query) use ($isFirst, $isForcedCaseInsensitive, $search): Builder { foreach ($this->getSearchColumns() as $searchColumn) { @@ -295,15 +298,21 @@ public function getOptionsLimit(): int return $this->optionsLimit; } - public function forceSearchCaseInsensitive(bool $condition = true): static + public function forceSearchCaseInsensitive(?bool $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; return $this; } - public function isSearchForcedCaseInsensitive(): bool + public function isSearchForcedCaseInsensitive(Builder $query): bool { - return $this->isSearchForcedCaseInsensitive; + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + return $this->isSearchForcedCaseInsensitive ?? match ($databaseConnection->getDriverName()) { + 'pgsql' => true, + default => false, + }; } } diff --git a/packages/forms/src/Components/Select.php b/packages/forms/src/Components/Select.php index 6b2d79d81d..a4f7554b1f 100644 --- a/packages/forms/src/Components/Select.php +++ b/packages/forms/src/Components/Select.php @@ -10,6 +10,7 @@ use Filament\Support\Concerns\HasExtraAlpineAttributes; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; @@ -101,7 +102,7 @@ class Select extends Field implements Contracts\HasAffixActions, Contracts\HasNe protected int | Closure $optionsLimit = 50; - protected bool | Closure $isSearchForcedCaseInsensitive = false; + protected bool | Closure | null $isSearchForcedCaseInsensitive = null; protected function setUp(): void { @@ -708,9 +709,13 @@ public function relationship(string | Closure | null $name, string | Closure | n $relationshipQuery->orderBy($relationshipQuery->qualifyColumn($relationshipTitleAttribute)); } + if ($component->isSearchForcedCaseInsensitive($relationshipQuery)) { + $search = Str::lower($search); + } + $component->applySearchConstraint( $relationshipQuery, - Str::lower($search), + $search, ); $baseRelationshipQuery = $relationshipQuery->getQuery(); @@ -1022,7 +1027,7 @@ static function (Select $component): bool { protected function applySearchConstraint(Builder $query, string $search): Builder { $isFirst = true; - $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive(); + $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive($query); $query->where(function (Builder $query) use ($isFirst, $isForcedCaseInsensitive, $search): Builder { foreach ($this->getSearchColumns() as $searchColumn) { @@ -1168,16 +1173,22 @@ public function getMaxItemsMessage(): string ]); } - public function forceSearchCaseInsensitive(bool | Closure $condition = true): static + public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; return $this; } - public function isSearchForcedCaseInsensitive(): bool + public function isSearchForcedCaseInsensitive(Builder $query): bool { - return (bool) $this->evaluate($this->isSearchForcedCaseInsensitive); + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + return $this->evaluate($this->isSearchForcedCaseInsensitive) ?? match ($databaseConnection->getDriverName()) { + 'pgsql' => true, + default => false, + }; } public function getDefaultState(): mixed diff --git a/packages/panels/docs/03-resources/08-global-search.md b/packages/panels/docs/03-resources/08-global-search.md index db45f5fa26..be2274bd7b 100644 --- a/packages/panels/docs/03-resources/08-global-search.md +++ b/packages/panels/docs/03-resources/08-global-search.md @@ -153,11 +153,3 @@ public function panel(Panel $panel): Panel ->globalSearchKeyBindings(['command+k', 'ctrl+k']); } ``` - -## Forcing case-insensitive global search - -By default, searching will use the sensitivity settings from the database table column. This is to avoid unnecessary performance overhead when searching large datasets that would arise if we were to force insensitivity for all users. However, if your database does not search case-insensitively by default, you can force it to by using the `$isGlobalSearchForcedCaseInsensitive` property: - -```php -protected static bool $isGlobalSearchForcedCaseInsensitive = true; -``` diff --git a/packages/panels/src/Resources/Resource.php b/packages/panels/src/Resources/Resource.php index 66cf5c7e6d..ca8e731e68 100644 --- a/packages/panels/src/Resources/Resource.php +++ b/packages/panels/src/Resources/Resource.php @@ -101,7 +101,7 @@ abstract class Resource protected static bool $shouldSkipAuthorization = false; - protected static bool $isGlobalSearchForcedCaseInsensitive = false; + protected static ?bool $isGlobalSearchForcedCaseInsensitive = null; public static function form(Form $form): Form { @@ -386,10 +386,12 @@ public static function getGlobalSearchResultsLimit(): int public static function getGlobalSearchResults(string $search): Collection { - $search = Str::lower($search); - $query = static::getGlobalSearchEloquentQuery(); + if (static::isGlobalSearchForcedCaseInsensitive($query)) { + $search = Str::lower($search); + } + foreach (explode(' ', $search) as $searchWord) { $query->where(function (Builder $query) use ($searchWord) { $isFirst = true; @@ -610,9 +612,15 @@ public static function hasRecordTitle(): bool return static::getRecordTitleAttribute() !== null; } - public static function isGlobalSearchForcedCaseInsensitive(): bool + public static function isGlobalSearchForcedCaseInsensitive(Builder $query): bool { - return static::$isGlobalSearchForcedCaseInsensitive; + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + return static::$isGlobalSearchForcedCaseInsensitive ?? match ($databaseConnection->getDriverName()) { + 'pgsql' => true, + default => false, + }; } /** @@ -625,18 +633,20 @@ protected static function applyGlobalSearchAttributeConstraint(Builder $query, s $model = $query->getModel(); + $isForcedCaseInsensitive = static::isGlobalSearchForcedCaseInsensitive($query); + foreach ($searchAttributes as $searchAttribute) { $whereClause = $isFirst ? 'where' : 'orWhere'; $query->when( method_exists($model, 'isTranslatableAttribute') && $model->isTranslatableAttribute($searchAttribute), - function (Builder $query) use ($databaseConnection, $searchAttribute, $search, $whereClause): Builder { + function (Builder $query) use ($databaseConnection, $isForcedCaseInsensitive, $searchAttribute, $search, $whereClause): Builder { $searchColumn = match ($databaseConnection->getDriverName()) { 'pgsql' => "{$searchAttribute}::text", default => $searchAttribute, }; - $caseAwareSearchColumn = static::isGlobalSearchForcedCaseInsensitive() ? + $caseAwareSearchColumn = $isForcedCaseInsensitive ? new Expression("lower({$searchColumn})") : $searchColumn; @@ -648,10 +658,10 @@ function (Builder $query) use ($databaseConnection, $searchAttribute, $search, $ }, fn (Builder $query): Builder => $query->when( str($searchAttribute)->contains('.'), - function ($query) use ($whereClause, $searchAttribute, $search) { + function (Builder $query) use ($isForcedCaseInsensitive, $searchAttribute, $search, $whereClause): Builder { $searchColumn = (string) str($searchAttribute)->afterLast('.'); - $caseAwareSearchColumn = static::isGlobalSearchForcedCaseInsensitive() ? + $caseAwareSearchColumn = $isForcedCaseInsensitive ? new Expression("lower({$searchColumn})") : $searchColumn; @@ -662,8 +672,8 @@ function ($query) use ($whereClause, $searchAttribute, $search) { "%{$search}%", ); }, - function ($query) use ($whereClause, $searchAttribute, $search) { - $caseAwareSearchColumn = static::isGlobalSearchForcedCaseInsensitive() ? + function ($query) use ($isForcedCaseInsensitive, $whereClause, $searchAttribute, $search) { + $caseAwareSearchColumn = $isForcedCaseInsensitive ? new Expression("lower({$searchAttribute})") : $searchAttribute; diff --git a/packages/tables/docs/03-columns/01-getting-started.md b/packages/tables/docs/03-columns/01-getting-started.md index 3c6dbcfdbf..9059808a05 100644 --- a/packages/tables/docs/03-columns/01-getting-started.md +++ b/packages/tables/docs/03-columns/01-getting-started.md @@ -229,18 +229,6 @@ public function table(Table $table): Table } ``` -### Forcing case-insensitive column search - -By default, searching will use the sensitivity settings from the database table column. This is to avoid unnecessary performance overhead when searching large datasets that would arise if we were to force insensitivity for all users. However, if your database does not search case-insensitively by default, you can force it to by using the `forceSearchCaseInsensitive()` method: - -```php -use Filament\Tables\Columns\TextColumn; - -TextColumn::make('name') - ->searchable() - ->forceSearchCaseInsensitive() -``` - ## Column actions and URLs When a cell is clicked, you may run an "action", or open a URL. diff --git a/packages/tables/src/Actions/AssociateAction.php b/packages/tables/src/Actions/AssociateAction.php index f40e01d02a..fb5fa5d999 100644 --- a/packages/tables/src/Actions/AssociateAction.php +++ b/packages/tables/src/Actions/AssociateAction.php @@ -7,6 +7,7 @@ use Filament\Forms\Components\Select; use Filament\Forms\Form; use Filament\Tables\Table; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -33,7 +34,7 @@ class AssociateAction extends Action */ protected array | Closure | null $recordSelectSearchColumns = null; - protected bool | Closure $isSearchForcedCaseInsensitive = false; + protected bool | Closure | null $isSearchForcedCaseInsensitive = null; public static function getDefaultName(): ?string { @@ -193,11 +194,13 @@ public function getRecordSelect(): Select } if (filled($search) && ($searchColumns || filled($titleAttribute))) { - $search = Str::lower($search); - $searchColumns ??= [$titleAttribute]; $isFirst = true; - $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive(); + $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive($relationshipQuery); + + if ($isForcedCaseInsensitive) { + $search = Str::lower($search); + } $relationshipQuery->where(function (Builder $query) use ($isFirst, $isForcedCaseInsensitive, $searchColumns, $search): Builder { foreach ($searchColumns as $searchColumn) { @@ -270,15 +273,21 @@ public function getRecordSelect(): Select return $select; } - public function forceSearchCaseInsensitive(bool | Closure $condition = true): static + public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; return $this; } - public function isSearchForcedCaseInsensitive(): bool + public function isSearchForcedCaseInsensitive(Builder $query): bool { - return (bool) $this->evaluate($this->isSearchForcedCaseInsensitive); + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + return $this->evaluate($this->isSearchForcedCaseInsensitive) ?? match ($databaseConnection->getDriverName()) { + 'pgsql' => true, + default => false, + }; } } diff --git a/packages/tables/src/Actions/AttachAction.php b/packages/tables/src/Actions/AttachAction.php index 9963b7a659..cb83b1c0da 100644 --- a/packages/tables/src/Actions/AttachAction.php +++ b/packages/tables/src/Actions/AttachAction.php @@ -7,6 +7,7 @@ use Filament\Forms\Components\Select; use Filament\Forms\Form; use Filament\Tables\Table; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; @@ -33,7 +34,7 @@ class AttachAction extends Action */ protected array | Closure | null $recordSelectSearchColumns = null; - protected bool | Closure $isSearchForcedCaseInsensitive = false; + protected bool | Closure | null $isSearchForcedCaseInsensitive = null; public static function getDefaultName(): ?string { @@ -229,11 +230,13 @@ public function getRecordSelect(): Select } if (filled($search) && ($searchColumns || filled($titleAttribute))) { - $search = Str::lower($search); - $searchColumns ??= [$titleAttribute]; $isFirst = true; - $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive(); + $isForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive($relationshipQuery); + + if ($isForcedCaseInsensitive) { + $search = Str::lower($search); + } $relationshipQuery->where(function (Builder $query) use ($isFirst, $isForcedCaseInsensitive, $searchColumns, $search): Builder { foreach ($searchColumns as $searchColumn) { @@ -322,15 +325,21 @@ public function getRecordSelect(): Select return $select; } - public function forceSearchCaseInsensitive(bool | Closure $condition = true): static + public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; return $this; } - public function isSearchForcedCaseInsensitive(): bool + public function isSearchForcedCaseInsensitive(Builder $query): bool { - return (bool) $this->evaluate($this->isSearchForcedCaseInsensitive); + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + return $this->evaluate($this->isSearchForcedCaseInsensitive) ?? match ($databaseConnection->getDriverName()) { + 'pgsql' => true, + default => false, + }; } } diff --git a/packages/tables/src/Columns/Concerns/CanBeSearchable.php b/packages/tables/src/Columns/Concerns/CanBeSearchable.php index ddf130faf0..e7d3df9e2d 100644 --- a/packages/tables/src/Columns/Concerns/CanBeSearchable.php +++ b/packages/tables/src/Columns/Concerns/CanBeSearchable.php @@ -3,6 +3,8 @@ namespace Filament\Tables\Columns\Concerns; use Closure; +use Illuminate\Database\Connection; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; trait CanBeSearchable @@ -20,7 +22,7 @@ trait CanBeSearchable protected ?Closure $searchQuery = null; - protected bool | Closure $isSearchForcedCaseInsensitive = false; + protected bool | Closure | null $isSearchForcedCaseInsensitive = null; /** * @param bool | array | string $condition @@ -46,7 +48,7 @@ public function searchable( return $this; } - public function forceSearchCaseInsensitive(bool | Closure $condition = true): static + public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; @@ -76,9 +78,15 @@ public function isIndividuallySearchable(): bool return $this->isSearchable() && $this->isIndividuallySearchable; } - public function isSearchForcedCaseInsensitive(): bool + public function isSearchForcedCaseInsensitive(Builder $query): bool { - return (bool) $this->evaluate($this->isSearchForcedCaseInsensitive); + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + return $this->evaluate($this->isSearchForcedCaseInsensitive) ?? match ($databaseConnection->getDriverName()) { + 'pgsql' => true, + default => false, + }; } /** diff --git a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php index 76ddf83933..265dc53ea8 100644 --- a/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php +++ b/packages/tables/src/Columns/Concerns/InteractsWithTableQuery.php @@ -3,6 +3,7 @@ namespace Filament\Tables\Columns\Concerns; use Exception; +use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -12,6 +13,7 @@ use Illuminate\Database\Query\Expression; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use Illuminate\Support\Stringable; trait InteractsWithTableQuery @@ -90,12 +92,21 @@ public function applySearchConstraint(EloquentBuilder $query, string $search, bo $query->when( $translatableContentDriver?->isAttributeTranslatable($model::class, attribute: $searchColumn), - fn (EloquentBuilder $query): EloquentBuilder => $translatableContentDriver->applySearchConstraintToQuery($query, $searchColumn, $search, $whereClause, $this->isSearchForcedCaseInsensitive()), + fn (EloquentBuilder $query): EloquentBuilder => $translatableContentDriver->applySearchConstraintToQuery($query, $searchColumn, $search, $whereClause, $this->isSearchForcedCaseInsensitive($query)), function (EloquentBuilder $query) use ($search, $searchColumn, $whereClause): EloquentBuilder { - $caseAwareSearchColumn = $this->isSearchForcedCaseInsensitive() ? + /** @var Connection $databaseConnection */ + $databaseConnection = $query->getConnection(); + + $isSearchForcedCaseInsensitive = $this->isSearchForcedCaseInsensitive($query); + + $caseAwareSearchColumn = $isSearchForcedCaseInsensitive ? new Expression("lower({$searchColumn})") : $searchColumn; + if ($isSearchForcedCaseInsensitive) { + $search = Str::lower($search); + } + return $query->when( $this->queriesRelationships($query->getModel()), fn (EloquentBuilder $query): EloquentBuilder => $query->{"{$whereClause}Relation"}( diff --git a/packages/tables/src/Concerns/CanSearchRecords.php b/packages/tables/src/Concerns/CanSearchRecords.php index bf81e911ba..b51ebcd941 100644 --- a/packages/tables/src/Concerns/CanSearchRecords.php +++ b/packages/tables/src/Concerns/CanSearchRecords.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; -use Illuminate\Support\Str; use RecursiveArrayIterator; use RecursiveIteratorIterator; @@ -106,7 +105,7 @@ protected function applyColumnSearchesToTableQuery(Builder $query): Builder protected function applyGlobalSearchToTableQuery(Builder $query): Builder { - $search = trim(Str::lower($this->getTableSearch())); + $search = trim($this->getTableSearch()); if (blank($search)) { return $query; @@ -249,7 +248,7 @@ public function getTableColumnSearches(): array // Nested array keys are flattened into `dot.syntax`. $searches[ implode('.', array_slice($path, 0, $iterator->getDepth() + 1)) - ] = trim(Str::lower($value)); + ] = trim($value); } return $searches; diff --git a/packages/tables/src/Filters/SelectFilter.php b/packages/tables/src/Filters/SelectFilter.php index 6a9ad07c82..90f4f2df8d 100644 --- a/packages/tables/src/Filters/SelectFilter.php +++ b/packages/tables/src/Filters/SelectFilter.php @@ -25,7 +25,7 @@ class SelectFilter extends BaseFilter protected int | Closure $optionsLimit = 50; - protected bool | Closure $isSearchForcedCaseInsensitive = false; + protected bool | Closure | null $isSearchForcedCaseInsensitive = null; protected function setUp(): void { @@ -184,16 +184,16 @@ public function getColumn(): string return $this->getAttribute(); } - public function forceSearchCaseInsensitive(bool | Closure $condition = true): static + public function forceSearchCaseInsensitive(bool | Closure | null $condition = true): static { $this->isSearchForcedCaseInsensitive = $condition; return $this; } - public function isSearchForcedCaseInsensitive(): bool + public function isSearchForcedCaseInsensitive(): ?bool { - return (bool) $this->evaluate($this->isSearchForcedCaseInsensitive); + return $this->evaluate($this->isSearchForcedCaseInsensitive); } public function getFormField(): Select