Skip to content

Commit 48bbdf7

Browse files
committed
Postgre Query Builder Optimization
1 parent 4c8d30e commit 48bbdf7

File tree

2 files changed

+210
-3
lines changed

2 files changed

+210
-3
lines changed

src/QueryBuilder/PostgresQueryBuilder.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,32 @@ public function insertBatch(array $data): PromiseInterface
179179
return AsyncPostgreSQL::execute($sql, $bindings);
180180
}
181181

182+
/**
183+
* PostgreSQL UPSERT operation (INSERT ... ON CONFLICT).
184+
*
185+
* @param array<string, mixed> $data The data to insert.
186+
* @param array<string> $conflictColumns Columns that define the conflict.
187+
* @param array<string, mixed> $updateData Data to update on conflict (use 'EXCLUDED' for excluded values).
188+
* @return PromiseInterface<int> A promise that resolves to the number of affected rows.
189+
*/
190+
public function upsert(array $data, array $conflictColumns, array $updateData = []): PromiseInterface
191+
{
192+
if ($data === []) {
193+
return Promise::resolve(0);
194+
}
195+
196+
$sql = $this->buildUpsertQuery($data, $conflictColumns, $updateData);
197+
$bindings = array_values($data);
198+
199+
foreach ($updateData as $value) {
200+
if ($value !== 'EXCLUDED') {
201+
$bindings[] = $value;
202+
}
203+
}
204+
205+
return AsyncPostgreSQL::execute($sql, $bindings);
206+
}
207+
182208
/**
183209
* Create a new record (alias for insert).
184210
*

src/QueryBuilder/PostgresQueryBuilderBase.php

Lines changed: 184 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use Rcalicdan\FiberAsync\QueryBuilder\Traits\SqlBuilderTrait;
1212

1313
/**
14-
* Abstract PostgreSQL query builder base with optimizations.
14+
* Abstract PostgreSQL query builder base with optimizations and PostgreSQL-specific features.
1515
*/
1616
abstract class PostgresQueryBuilderBase
1717
{
@@ -30,6 +30,8 @@ abstract class PostgresQueryBuilderBase
3030

3131
/**
3232
* Generate PostgreSQL parameter placeholder ($1, $2, $3, etc.)
33+
*
34+
* @return string The placeholder string.
3335
*/
3436
protected function getPlaceholder(): string
3537
{
@@ -38,14 +40,21 @@ protected function getPlaceholder(): string
3840

3941
/**
4042
* Reset parameter counter for new query
43+
*
44+
* @return void
4145
*/
4246
protected function resetParameterCount(): void
4347
{
4448
$this->parameterCount = 0;
4549
}
4650

4751
/**
48-
* PostgreSQL ILIKE for case-insensitive matching
52+
* PostgreSQL ILIKE for case-insensitive matching.
53+
*
54+
* @param string $column The column name.
55+
* @param string $value The value to search for.
56+
* @param string $side The side to add wildcards ('before', 'after', 'both').
57+
* @return static Returns a new query builder instance for method chaining.
4958
*/
5059
public function ilike(string $column, string $value, string $side = 'both'): static
5160
{
@@ -65,7 +74,12 @@ public function ilike(string $column, string $value, string $side = 'both'): sta
6574
}
6675

6776
/**
68-
* PostgreSQL JSON field access
77+
* PostgreSQL JSON field access using -> operator.
78+
*
79+
* @param string $column The JSON column name.
80+
* @param string $path The JSON path to access.
81+
* @param mixed $value The value to compare against.
82+
* @return static Returns a new query builder instance for method chaining.
6983
*/
7084
public function whereJson(string $column, string $path, mixed $value): static
7185
{
@@ -76,8 +90,113 @@ public function whereJson(string $column, string $path, mixed $value): static
7690
return $instance;
7791
}
7892

93+
/**
94+
* PostgreSQL JSON field text extraction using ->> operator.
95+
*
96+
* @param string $column The JSON column name.
97+
* @param string $path The JSON path to access.
98+
* @param mixed $value The value to compare against.
99+
* @return static Returns a new query builder instance for method chaining.
100+
*/
101+
public function whereJsonEquals(string $column, string $path, mixed $value): static
102+
{
103+
$instance = clone $this;
104+
$placeholder1 = $instance->getPlaceholder();
105+
$placeholder2 = $instance->getPlaceholder();
106+
$instance->where[] = "{$column}->>{$placeholder1} = {$placeholder2}";
107+
$instance->bindings['where'][] = $path;
108+
$instance->bindings['where'][] = $value;
109+
return $instance;
110+
}
111+
112+
/**
113+
* PostgreSQL JSON contains operator (@>).
114+
*
115+
* @param string $column The JSON column name.
116+
* @param array<string, mixed> $value The JSON value to check containment.
117+
* @return static Returns a new query builder instance for method chaining.
118+
*/
119+
public function whereJsonContains(string $column, array $value): static
120+
{
121+
$instance = clone $this;
122+
$placeholder = $instance->getPlaceholder();
123+
$instance->where[] = "{$column} @> {$placeholder}::jsonb";
124+
$instance->bindings['where'][] = json_encode($value);
125+
return $instance;
126+
}
127+
128+
/**
129+
* PostgreSQL JSON key exists operator (?).
130+
*
131+
* @param string $column The JSON column name.
132+
* @param string $key The key to check for existence.
133+
* @return static Returns a new query builder instance for method chaining.
134+
*/
135+
public function whereJsonHasKey(string $column, string $key): static
136+
{
137+
$instance = clone $this;
138+
$placeholder = $instance->getPlaceholder();
139+
$instance->where[] = "{$column} ? {$placeholder}";
140+
$instance->bindings['where'][] = $key;
141+
return $instance;
142+
}
143+
144+
/**
145+
* PostgreSQL array contains using ANY operator.
146+
*
147+
* @param string $column The array column name.
148+
* @param mixed $value The value to search for in the array.
149+
* @return static Returns a new query builder instance for method chaining.
150+
*/
151+
public function whereArrayContains(string $column, mixed $value): static
152+
{
153+
$instance = clone $this;
154+
$placeholder = $instance->getPlaceholder();
155+
$instance->where[] = "{$placeholder} = ANY({$column})";
156+
$instance->bindings['where'][] = $value;
157+
return $instance;
158+
}
159+
160+
/**
161+
* PostgreSQL array overlap operator (&&).
162+
*
163+
* @param string $column The array column name.
164+
* @param array<mixed> $values The values to check for overlap.
165+
* @return static Returns a new query builder instance for method chaining.
166+
*/
167+
public function whereArrayOverlap(string $column, array $values): static
168+
{
169+
$instance = clone $this;
170+
$placeholder = $instance->getPlaceholder();
171+
$instance->where[] = "{$column} && {$placeholder}::text[]";
172+
$instance->bindings['where'][] = '{' . implode(',', array_map(fn($v) => '"' . addslashes($v) . '"', $values)) . '}';
173+
return $instance;
174+
}
175+
176+
/**
177+
* PostgreSQL full-text search using tsvector and tsquery.
178+
*
179+
* @param string $column The column to search in.
180+
* @param string $query The search query.
181+
* @param string $config The text search configuration (default: 'english').
182+
* @return static Returns a new query builder instance for method chaining.
183+
*/
184+
public function whereFullText(string $column, string $query, string $config = 'english'): static
185+
{
186+
$instance = clone $this;
187+
$placeholder1 = $instance->getPlaceholder();
188+
$placeholder2 = $instance->getPlaceholder();
189+
190+
$instance->where[] = "to_tsvector({$placeholder1}, {$column}) @@ plainto_tsquery({$placeholder2})";
191+
$instance->bindings['where'][] = $config;
192+
$instance->bindings['where'][] = $query;
193+
return $instance;
194+
}
195+
79196
/**
80197
* Override buildSelectQuery to reset parameter count and optimize for PostgreSQL
198+
*
199+
* @return string The complete SELECT SQL query.
81200
*/
82201
protected function buildSelectQuery(): string
83202
{
@@ -123,6 +242,9 @@ protected function buildSelectQuery(): string
123242

124243
/**
125244
* Override buildCountQuery for PostgreSQL
245+
*
246+
* @param string $column The column to count.
247+
* @return string The complete COUNT SQL query.
126248
*/
127249
protected function buildCountQuery(string $column = '*'): string
128250
{
@@ -156,6 +278,9 @@ protected function buildCountQuery(string $column = '*'): string
156278

157279
/**
158280
* Override buildInsertQuery for PostgreSQL
281+
*
282+
* @param array<string, mixed> $data The data to insert.
283+
* @return string The complete INSERT SQL query.
159284
*/
160285
protected function buildInsertQuery(array $data): string
161286
{
@@ -174,6 +299,11 @@ protected function buildInsertQuery(array $data): string
174299

175300
/**
176301
* Override buildInsertBatchQuery for PostgreSQL
302+
*
303+
* @param array<array<string, mixed>> $data The data array for batch insert.
304+
* @return string The complete INSERT SQL query.
305+
*
306+
* @throws \InvalidArgumentException When data format is invalid.
177307
*/
178308
protected function buildInsertBatchQuery(array $data): string
179309
{
@@ -201,6 +331,9 @@ protected function buildInsertBatchQuery(array $data): string
201331

202332
/**
203333
* Override buildUpdateQuery for PostgreSQL
334+
*
335+
* @param array<string, mixed> $data The data to update.
336+
* @return string The complete UPDATE SQL query.
204337
*/
205338
protected function buildUpdateQuery(array $data): string
206339
{
@@ -223,6 +356,8 @@ protected function buildUpdateQuery(array $data): string
223356

224357
/**
225358
* Override buildDeleteQuery for PostgreSQL
359+
*
360+
* @return string The complete DELETE SQL query.
226361
*/
227362
protected function buildDeleteQuery(): string
228363
{
@@ -240,9 +375,55 @@ protected function buildDeleteQuery(): string
240375

241376
/**
242377
* Build INSERT with RETURNING clause for PostgreSQL
378+
*
379+
* @param array<string, mixed> $data The data to insert.
380+
* @param string $returning The column to return.
381+
* @return string The complete INSERT SQL query with RETURNING clause.
243382
*/
244383
protected function buildInsertReturningQuery(array $data, string $returning = 'id'): string
245384
{
246385
return $this->buildInsertQuery($data) . " RETURNING {$returning}";
247386
}
387+
388+
/**
389+
* Build PostgreSQL UPSERT query (INSERT ... ON CONFLICT).
390+
*
391+
* @param array<string, mixed> $data The data to insert.
392+
* @param array<string> $conflictColumns Columns that define the conflict.
393+
* @param array<string, mixed> $updateData Data to update on conflict (optional).
394+
* @return string The complete UPSERT SQL query.
395+
*/
396+
protected function buildUpsertQuery(array $data, array $conflictColumns, array $updateData = []): string
397+
{
398+
$this->resetParameterCount();
399+
400+
$columns = implode(', ', array_keys($data));
401+
$placeholders = [];
402+
403+
foreach ($data as $value) {
404+
$placeholders[] = $this->getPlaceholder();
405+
}
406+
407+
$placeholderString = implode(', ', $placeholders);
408+
$conflictCols = implode(', ', $conflictColumns);
409+
410+
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholderString})";
411+
$sql .= " ON CONFLICT ({$conflictCols})";
412+
413+
if (empty($updateData)) {
414+
$sql .= " DO NOTHING";
415+
} else {
416+
$updateClauses = [];
417+
foreach (array_keys($updateData) as $column) {
418+
if (isset($updateData[$column]) && $updateData[$column] === 'EXCLUDED') {
419+
$updateClauses[] = "{$column} = EXCLUDED.{$column}";
420+
} else {
421+
$updateClauses[] = "{$column} = " . $this->getPlaceholder();
422+
}
423+
}
424+
$sql .= " DO UPDATE SET " . implode(', ', $updateClauses);
425+
}
426+
427+
return $sql;
428+
}
248429
}

0 commit comments

Comments
 (0)