Skip to content

Commit

Permalink
Merge pull request #7849 from filamentphp/postgres-case-insensitivity
Browse files Browse the repository at this point in the history
fix: Postgres search case insensitivity
  • Loading branch information
danharrin authored Aug 17, 2023
2 parents 4a6414a + d409b17 commit 1280c89
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 84 deletions.
13 changes: 0 additions & 13 deletions packages/forms/docs/03-fields/03-select.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 16 additions & 7 deletions packages/forms/src/Components/MorphToSelect/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,7 +37,7 @@ class Type

protected string $model;

protected bool $isSearchForcedCaseInsensitive = false;
protected ?bool $isSearchForcedCaseInsensitive = null;

final public function __construct(string $model)
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
};
}
}
23 changes: 17 additions & 6 deletions packages/forms/src/Components/Select.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
8 changes: 0 additions & 8 deletions packages/panels/docs/03-resources/08-global-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
```
32 changes: 21 additions & 11 deletions packages/panels/src/Resources/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};
}

/**
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down
12 changes: 0 additions & 12 deletions packages/tables/docs/03-columns/01-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 16 additions & 7 deletions packages/tables/src/Actions/AssociateAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
};
}
}
23 changes: 16 additions & 7 deletions packages/tables/src/Actions/AttachAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
};
}
}
Loading

0 comments on commit 1280c89

Please sign in to comment.