diff --git a/packages/support/src/helpers.php b/packages/support/src/helpers.php index e0362a5f6a..05eeb77d59 100644 --- a/packages/support/src/helpers.php +++ b/packages/support/src/helpers.php @@ -166,26 +166,38 @@ if (! function_exists('Filament\Support\generate_search_column_expression')) { $column = match ($driverName) { 'pgsql' => ( str($column)->contains('->') - ? ( - // Handle `table.field` part with double quotes - str($column) - ->before('->') - ->explode('.') - ->map(fn (string $part): string => (string) str($part)->wrap('"')) - ->implode('.') - ) . collect(str($column)->after('->')->explode('->')) // Handle JSON path parts - ->map(function ($segment, $index) use ($column): string { - $totalParts = substr_count($column, '->'); + ? ( + // Handle `table.field` part with double quotes + str($column) + ->before('->') + ->explode('.') + ->map(fn (string $part): string => (string) str($part)->wrap('"')) + ->implode('.') + ) . collect(str($column)->after('->')->explode('->')) // Handle JSON path parts + ->map(function ($segment, $index) use ($column): string { + // If segment already starts with `>` (from `->>` operator), preserve it + $isExplicitOperatorPrefixed = str($segment)->startsWith('>'); + $segment = $isExplicitOperatorPrefixed ? (string) str($segment)->after('>') : $segment; - return ($index === ($totalParts - 1)) - ? "->>'{$segment}'" - : "->'{$segment}'"; - }) - ->implode('') - : str($column) - ->explode('.') - ->map(fn (string $part): string => (string) str($part)->wrap('"')) - ->implode('.') + // Remove single quotes from segment if present to avoid redundant quoting + $isWrappedWithSingleQuotes = str($segment)->startsWith("'") && str($segment)->endsWith("'"); + $segment = $isWrappedWithSingleQuotes ? (string) str($segment)->trim("'") : $segment; + + if ($isExplicitOperatorPrefixed) { + return "->>'{$segment}'"; + } + + $totalParts = substr_count($column, '->'); + + return ($index === ($totalParts - 1)) + ? "->>'{$segment}'" + : "->'{$segment}'"; + }) + ->implode('') + : str($column) + ->explode('.') + ->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 fb9f27ba7e..e26449a189 100644 --- a/tests/src/Support/helpersTest.php +++ b/tests/src/Support/helpersTest.php @@ -112,3 +112,51 @@ it('will generate a column expression for Postgres with colons in the table name ['blog:posts.title', 'lower("blog:posts"."title"::text)'], ['blog:posts:comments.author.name', 'lower("blog:posts:comments"."author"."name"::text)'], ]); + +it('will generate a JSON search column expression for Postgres with explicit ->> operator', function () { + $column = 'data->>name'; + $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'::text)"); +}); + +it('will generate a nested JSON search column expression for Postgres with explicit ->> operator on the last segment', function () { + $column = 'data->name->>ar'; + $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'->>'ar'::text)"); +}); + +it('will generate a JSON search column expression for Postgres with explicit ->> operator and simple key', function () { + $column = 'name->>\'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(\"name\"->>'en'::text)"); +});