From e4bbf97ce992c5a92a298b26e3b106bf749f3abb Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Thu, 10 Jul 2025 09:38:05 +0100 Subject: [PATCH 1/3] Handle Postgres nested JSON searches --- packages/support/src/helpers.php | 23 ++++++++++++++++------- tests/src/Support/helpersTest.php | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/support/src/helpers.php b/packages/support/src/helpers.php index 62c72e6339..36e0374cec 100644 --- a/packages/support/src/helpers.php +++ b/packages/support/src/helpers.php @@ -166,17 +166,26 @@ if (! function_exists('Filament\Support\generate_search_column_expression')) { $column = match ($driverName) { 'pgsql' => ( str($column)->contains('->') - ? str( + ? ( + // Handle table.field part with double quotes str($column) - ->beforeLast('->') + ->before('->') ->explode('.') ->map(fn (string $part) => str($part)->wrap('"')) ->implode('.') - ) - ->append('->>') - ->append("'") - ->append(str($column)->afterLast('->')) - ->append("'") + ) . + // Handle JSON path parts + collect(str($column)->after('->')->explode('->')) + ->map(function ($segment, $index) use ($column) { + $totalParts = substr_count($column, '->'); + + return ($index === $totalParts - 1) + ? "->>'$segment'" + : "->'$segment'"; + }) + ->prepend('') + ->implode('') + : str($column) ->explode('.') ->map(fn (string $part) => str($part)->wrap('"')) diff --git a/tests/src/Support/helpersTest.php b/tests/src/Support/helpersTest.php index a5c8d2a29a..8a4fef0635 100644 --- a/tests/src/Support/helpersTest.php +++ b/tests/src/Support/helpersTest.php @@ -78,6 +78,22 @@ it('will generate json search column expression for pgsql', function () { ->toBe("lower(\"data\"->>'name'::text)"); }); +it('will generate nested json search column expression for pgsql', function () { + $column = 'data->name->value->en'; + $isSearchForcedCaseInsensitive = true; + + $databaseConnection = Mockery::mock(Connection::class); + $databaseConnection->shouldReceive('getDriverName')->andReturn('pgsql'); + $databaseConnection->shouldReceive('getConfig')->with('search_collation')->andReturn(null); + + $grammar = new PostgresGrammar($databaseConnection); + + $expression = generate_search_column_expression($column, $isSearchForcedCaseInsensitive, $databaseConnection); + + expect($expression->getValue($grammar)) + ->toBe("lower(\"data\"->'name'->'value'->>'en'::text)"); +}); + it('will generate column expression for pgsql with colons in the name', function (string $column, string $text) { $isSearchForcedCaseInsensitive = true; From 03c26405ef6c867b50e487b05aa999cc1d996c23 Mon Sep 17 00:00:00 2001 From: Glenn Jacobs Date: Thu, 10 Jul 2025 09:46:20 +0100 Subject: [PATCH 2/3] Update helpers.php --- packages/support/src/helpers.php | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/support/src/helpers.php b/packages/support/src/helpers.php index 36e0374cec..3650cb4490 100644 --- a/packages/support/src/helpers.php +++ b/packages/support/src/helpers.php @@ -183,7 +183,6 @@ if (! function_exists('Filament\Support\generate_search_column_expression')) { ? "->>'$segment'" : "->'$segment'"; }) - ->prepend('') ->implode('') : str($column) From c4ea3093be650f5b03b9c2d6f43ffc1d3fe18c63 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Thu, 10 Jul 2025 10:47:42 +0100 Subject: [PATCH 3/3] clean up --- packages/support/src/helpers.php | 19 ++++++++----------- tests/src/Support/helpersTest.php | 14 +++++++------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/support/src/helpers.php b/packages/support/src/helpers.php index 3650cb4490..e0362a5f6a 100644 --- a/packages/support/src/helpers.php +++ b/packages/support/src/helpers.php @@ -167,27 +167,24 @@ if (! function_exists('Filament\Support\generate_search_column_expression')) { 'pgsql' => ( str($column)->contains('->') ? ( - // Handle table.field part with double quotes + // Handle `table.field` part with double quotes str($column) ->before('->') ->explode('.') - ->map(fn (string $part) => str($part)->wrap('"')) + ->map(fn (string $part): string => (string) str($part)->wrap('"')) ->implode('.') - ) . - // Handle JSON path parts - collect(str($column)->after('->')->explode('->')) - ->map(function ($segment, $index) use ($column) { + ) . collect(str($column)->after('->')->explode('->')) // Handle JSON path parts + ->map(function ($segment, $index) use ($column): string { $totalParts = substr_count($column, '->'); - return ($index === $totalParts - 1) - ? "->>'$segment'" - : "->'$segment'"; + return ($index === ($totalParts - 1)) + ? "->>'{$segment}'" + : "->'{$segment}'"; }) ->implode('') - : str($column) ->explode('.') - ->map(fn (string $part) => str($part)->wrap('"')) + ->map(fn (string $part): string => (string) str($part)->wrap('"')) ->implode('.') ) . '::text', default => $column, diff --git a/tests/src/Support/helpersTest.php b/tests/src/Support/helpersTest.php index 8a4fef0635..fb9f27ba7e 100644 --- a/tests/src/Support/helpersTest.php +++ b/tests/src/Support/helpersTest.php @@ -44,7 +44,7 @@ it('will prepare data attributes', function () { ]); }); -it('will generate json search column expression for mysql', function () { +it('will generate a JSON search column expression for MySQL', function () { $column = 'data->name'; $isSearchForcedCaseInsensitive = true; @@ -62,7 +62,7 @@ it('will generate json search column expression for mysql', function () { ->toBe("lower(json_extract(`data`, '$.\"name\"'))"); }); -it('will generate json search column expression for pgsql', function () { +it('will generate a JSON search column expression for Postgres', function () { $column = 'data->name'; $isSearchForcedCaseInsensitive = true; @@ -78,7 +78,7 @@ it('will generate json search column expression for pgsql', function () { ->toBe("lower(\"data\"->>'name'::text)"); }); -it('will generate nested json search column expression for pgsql', function () { +it('will generate a nested JSON search column expression for Postgres', function () { $column = 'data->name->value->en'; $isSearchForcedCaseInsensitive = true; @@ -94,7 +94,7 @@ it('will generate nested json search column expression for pgsql', function () { ->toBe("lower(\"data\"->'name'->'value'->>'en'::text)"); }); -it('will generate column expression for pgsql with colons in the name', function (string $column, string $text) { +it('will generate a column expression for Postgres with colons in the table name', function (string $column, string $expectedExpression) { $isSearchForcedCaseInsensitive = true; $databaseConnection = Mockery::mock(Connection::class); @@ -103,10 +103,10 @@ it('will generate column expression for pgsql with colons in the name', function $grammar = new PostgresGrammar($databaseConnection); - $expression = generate_search_column_expression($column, $isSearchForcedCaseInsensitive, $databaseConnection); + $actualExpression = generate_search_column_expression($column, $isSearchForcedCaseInsensitive, $databaseConnection); - expect($expression->getValue($grammar)) - ->toBe($text); + expect($actualExpression->getValue($grammar)) + ->toBe($expectedExpression); }) ->with([ ['blog:posts.title', 'lower("blog:posts"."title"::text)'],