11
11
use Rcalicdan \FiberAsync \QueryBuilder \Traits \SqlBuilderTrait ;
12
12
13
13
/**
14
- * Abstract PostgreSQL query builder base with optimizations.
14
+ * Abstract PostgreSQL query builder base with optimizations and PostgreSQL-specific features .
15
15
*/
16
16
abstract class PostgresQueryBuilderBase
17
17
{
@@ -30,6 +30,8 @@ abstract class PostgresQueryBuilderBase
30
30
31
31
/**
32
32
* Generate PostgreSQL parameter placeholder ($1, $2, $3, etc.)
33
+ *
34
+ * @return string The placeholder string.
33
35
*/
34
36
protected function getPlaceholder (): string
35
37
{
@@ -38,14 +40,21 @@ protected function getPlaceholder(): string
38
40
39
41
/**
40
42
* Reset parameter counter for new query
43
+ *
44
+ * @return void
41
45
*/
42
46
protected function resetParameterCount (): void
43
47
{
44
48
$ this ->parameterCount = 0 ;
45
49
}
46
50
47
51
/**
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.
49
58
*/
50
59
public function ilike (string $ column , string $ value , string $ side = 'both ' ): static
51
60
{
@@ -65,7 +74,12 @@ public function ilike(string $column, string $value, string $side = 'both'): sta
65
74
}
66
75
67
76
/**
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.
69
83
*/
70
84
public function whereJson (string $ column , string $ path , mixed $ value ): static
71
85
{
@@ -76,8 +90,113 @@ public function whereJson(string $column, string $path, mixed $value): static
76
90
return $ instance ;
77
91
}
78
92
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
+
79
196
/**
80
197
* Override buildSelectQuery to reset parameter count and optimize for PostgreSQL
198
+ *
199
+ * @return string The complete SELECT SQL query.
81
200
*/
82
201
protected function buildSelectQuery (): string
83
202
{
@@ -123,6 +242,9 @@ protected function buildSelectQuery(): string
123
242
124
243
/**
125
244
* Override buildCountQuery for PostgreSQL
245
+ *
246
+ * @param string $column The column to count.
247
+ * @return string The complete COUNT SQL query.
126
248
*/
127
249
protected function buildCountQuery (string $ column = '* ' ): string
128
250
{
@@ -156,6 +278,9 @@ protected function buildCountQuery(string $column = '*'): string
156
278
157
279
/**
158
280
* Override buildInsertQuery for PostgreSQL
281
+ *
282
+ * @param array<string, mixed> $data The data to insert.
283
+ * @return string The complete INSERT SQL query.
159
284
*/
160
285
protected function buildInsertQuery (array $ data ): string
161
286
{
@@ -174,6 +299,11 @@ protected function buildInsertQuery(array $data): string
174
299
175
300
/**
176
301
* 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.
177
307
*/
178
308
protected function buildInsertBatchQuery (array $ data ): string
179
309
{
@@ -201,6 +331,9 @@ protected function buildInsertBatchQuery(array $data): string
201
331
202
332
/**
203
333
* Override buildUpdateQuery for PostgreSQL
334
+ *
335
+ * @param array<string, mixed> $data The data to update.
336
+ * @return string The complete UPDATE SQL query.
204
337
*/
205
338
protected function buildUpdateQuery (array $ data ): string
206
339
{
@@ -223,6 +356,8 @@ protected function buildUpdateQuery(array $data): string
223
356
224
357
/**
225
358
* Override buildDeleteQuery for PostgreSQL
359
+ *
360
+ * @return string The complete DELETE SQL query.
226
361
*/
227
362
protected function buildDeleteQuery (): string
228
363
{
@@ -240,9 +375,55 @@ protected function buildDeleteQuery(): string
240
375
241
376
/**
242
377
* 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.
243
382
*/
244
383
protected function buildInsertReturningQuery (array $ data , string $ returning = 'id ' ): string
245
384
{
246
385
return $ this ->buildInsertQuery ($ data ) . " RETURNING {$ returning }" ;
247
386
}
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
+ }
248
429
}
0 commit comments