Skip to content

Commit 1b4c563

Browse files
committed
wip
1 parent 44f512a commit 1b4c563

File tree

5 files changed

+158
-7
lines changed

5 files changed

+158
-7
lines changed

.github/workflows/run-tests.yml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
php: [8.4, 8.3, 8.2]
1515
laravel: ["11.*", "12.*"]
1616
dependency-version: [prefer-lowest, prefer-stable]
17-
db_connection: [mysql, sqlite]
17+
db_connection: [mysql, pgsql, sqlite]
1818
include:
1919
- laravel: 11.*
2020
testbench: ^9.10
@@ -35,6 +35,20 @@ jobs:
3535
ports:
3636
- 3306
3737
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
38+
39+
postgres:
40+
image: postgres:15
41+
env:
42+
POSTGRES_USER: protone_media_db_test
43+
POSTGRES_PASSWORD: secret
44+
POSTGRES_DB: protone_media_db_test
45+
ports:
46+
- 5432:5432
47+
options: >-
48+
--health-cmd pg_isready
49+
--health-interval 10s
50+
--health-timeout 5s
51+
--health-retries 5
3852
3953
steps:
4054
- name: Checkout code
@@ -50,7 +64,7 @@ jobs:
5064
uses: shivammathur/setup-php@v2
5165
with:
5266
php-version: ${{ matrix.php }}
53-
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql
67+
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql, pgsql, pdo_pgsql
5468
coverage: none
5569

5670
- name: Install dependencies
@@ -65,4 +79,4 @@ jobs:
6579
DB_DATABASE: protone_media_db_test
6680
DB_USERNAME: protone_media_db_test
6781
DB_PASSWORD: secret
68-
DB_PORT: ${{ job.services.mysql.ports[3306] }}
82+
DB_PORT: ${{ matrix.db_connection == 'pgsql' && 5432 || job.services.mysql.ports[3306] }}

src/DatabaseGrammarFactory.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Database\Connection;
88
use ProtoneMedia\LaravelCrossEloquentSearch\Exceptions\InvalidGrammarException;
99
use ProtoneMedia\LaravelCrossEloquentSearch\Grammars\MySqlSearchGrammar;
10+
use ProtoneMedia\LaravelCrossEloquentSearch\Grammars\PostgreSqlSearchGrammar;
1011
use ProtoneMedia\LaravelCrossEloquentSearch\Grammars\SearchGrammarInterface;
1112
use ProtoneMedia\LaravelCrossEloquentSearch\Grammars\SQLiteSearchGrammar;
1213

@@ -15,7 +16,7 @@
1516
*
1617
* This factory provides database-agnostic access to search functionality by
1718
* creating the appropriate grammar implementation based on the database driver.
18-
* Currently supports MySQL/MariaDB and SQLite.
19+
* Currently supports MySQL/MariaDB, PostgreSQL, and SQLite.
1920
*/
2021
class DatabaseGrammarFactory
2122
{
@@ -32,6 +33,8 @@ public static function make(Connection $connection): SearchGrammarInterface
3233
case 'mysql':
3334
case 'mariadb':
3435
return new MySqlSearchGrammar($connection);
36+
case 'pgsql':
37+
return new PostgreSqlSearchGrammar($connection);
3538
case 'sqlite':
3639
return new SQLiteSearchGrammar($connection);
3740
default:
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ProtoneMedia\LaravelCrossEloquentSearch\Grammars;
6+
7+
use Illuminate\Contracts\Database\Query\Expression;
8+
use Illuminate\Database\Connection;
9+
use Illuminate\Database\Query\Grammars\PostgresGrammar;
10+
11+
/**
12+
* PostgreSQL-specific search grammar implementation.
13+
*/
14+
class PostgreSqlSearchGrammar implements SearchGrammarInterface
15+
{
16+
protected PostgresGrammar $grammar;
17+
18+
/**
19+
* Create a new PostgreSQL search grammar instance.
20+
*/
21+
public function __construct(protected Connection $connection)
22+
{
23+
$this->grammar = new PostgresGrammar($this->connection);
24+
}
25+
26+
public function wrap(string|Expression $value): string
27+
{
28+
return $this->grammar->wrap($value);
29+
}
30+
31+
/**
32+
* Create a case-insensitive column expression.
33+
*/
34+
public function caseInsensitive(string $column): string
35+
{
36+
return "LOWER({$column})";
37+
}
38+
39+
/**
40+
* @param array<int, string> $values
41+
*/
42+
public function coalesce(array $values): string
43+
{
44+
$valueList = implode(',', $values);
45+
46+
return "COALESCE({$valueList})";
47+
}
48+
49+
/**
50+
* Create a character length expression for the given column.
51+
*/
52+
public function charLength(string $column): string
53+
{
54+
return "CHAR_LENGTH({$column})";
55+
}
56+
57+
/**
58+
* Create a string replace expression.
59+
*/
60+
public function replace(string $column, string $search, string $replace): string
61+
{
62+
return "REPLACE({$column}, {$search}, {$replace})";
63+
}
64+
65+
/**
66+
* Create a lowercase expression for the given column.
67+
*/
68+
public function lower(string $column): string
69+
{
70+
return "LOWER({$column})";
71+
}
72+
73+
/**
74+
* Get the operator used for phonetic/sounds-like matching.
75+
*/
76+
public function soundsLikeOperator(): string
77+
{
78+
// PostgreSQL doesn't have a built-in SOUNDS LIKE operator
79+
// Could use extensions like fuzzystrmatch with soundex() function
80+
// For now, fall back to ILIKE for case-insensitive matching
81+
return 'ilike';
82+
}
83+
84+
/**
85+
* Check if the database supports phonetic/sounds-like matching.
86+
*/
87+
public function supportsSoundsLike(): bool
88+
{
89+
// PostgreSQL supports phonetic matching through extensions like fuzzystrmatch
90+
// However, we'll return false for the basic implementation to keep it simple
91+
// Users can enable the extension and customize this if needed
92+
return false;
93+
}
94+
95+
/**
96+
* Check if the database supports complex ordering in UNION queries.
97+
*/
98+
public function supportsUnionOrdering(): bool
99+
{
100+
// PostgreSQL doesn't support complex ORDER BY expressions in UNION queries
101+
// Similar to SQLite, we need to wrap the UNION in a subquery
102+
return false;
103+
}
104+
105+
/**
106+
* Wrap a UNION query for databases that need special handling.
107+
*
108+
* @param array<int, mixed> $bindings
109+
* @return array<string, mixed>
110+
*/
111+
public function wrapUnionQuery(string $sql, array $bindings): array
112+
{
113+
return [
114+
'sql' => "({$sql}) as union_results",
115+
'bindings' => $bindings,
116+
];
117+
}
118+
}

tests/SearchTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,9 @@ public function it_can_search_on_the_left_side_of_the_term()
225225
/** @test */
226226
public function it_can_use_the_sounds_like_operator()
227227
{
228-
// Skip on SQLite since it doesn't support SOUNDS LIKE
229-
if (config('database.default') === 'sqlite') {
230-
$this->markTestSkipped('SOUNDS LIKE operator not supported on SQLite');
228+
// Skip on SQLite and PostgreSQL since they don't support SOUNDS LIKE by default
229+
if (in_array(config('database.default'), ['sqlite', 'pgsql'])) {
230+
$this->markTestSkipped('SOUNDS LIKE operator not supported on '.config('database.default'));
231231
}
232232

233233
Video::create(['title' => 'laravel']);

tests/TestCase.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,22 @@ protected function initDatabase($prefix = '')
6060
]) : [],
6161
]);
6262

63+
// Configure PostgreSQL
64+
$this->app['config']->set('database.connections.pgsql', [
65+
'driver' => 'pgsql',
66+
'url' => env('DATABASE_URL'),
67+
'host' => env('DB_HOST', '127.0.0.1'),
68+
'port' => env('DB_PORT', '5432'),
69+
'database' => env('DB_DATABASE', 'search_test'),
70+
'username' => env('DB_USERNAME', 'protone_media_db_test'),
71+
'password' => env('DB_PASSWORD', 'secret'),
72+
'charset' => 'utf8',
73+
'prefix' => $prefix,
74+
'prefix_indexes' => true,
75+
'search_path' => 'public',
76+
'sslmode' => 'prefer',
77+
]);
78+
6379
// Set default connection based on DB_CONNECTION env var
6480
$this->app['config']->set('database.default', $connection);
6581

0 commit comments

Comments
 (0)