1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This file is part of Cycle ORM package. |
||
5 | * |
||
6 | * For the full copyright and license information, please view the LICENSE |
||
7 | * file that was distributed with this source code. |
||
8 | */ |
||
9 | |||
10 | declare(strict_types=1); |
||
11 | |||
12 | namespace Cycle\Database\Driver; |
||
13 | |||
14 | use Cycle\Database\Exception\CompilerException; |
||
15 | use Cycle\Database\Injection\FragmentInterface; |
||
16 | use Cycle\Database\Injection\Parameter; |
||
17 | use Cycle\Database\Injection\ParameterInterface; |
||
18 | use Cycle\Database\Query\QueryParameters; |
||
19 | |||
20 | abstract class Compiler implements CompilerInterface |
||
21 | { |
||
22 | protected const ORDER_OPTIONS = ['ASC', 'DESC']; |
||
23 | |||
24 | private Quoter $quoter; |
||
25 | |||
26 | /** |
||
27 | 82 | * @psalm-param non-empty-string $quotes |
|
28 | */ |
||
29 | 82 | public function __construct(string $quotes = '""') |
|
30 | 82 | { |
|
31 | $this->quoter = new Quoter('', $quotes); |
||
32 | } |
||
33 | |||
34 | /** |
||
35 | * @psalm-param non-empty-string $identifier |
||
36 | * |
||
37 | 3460 | * @psalm-return non-empty-string |
|
38 | */ |
||
39 | 3460 | public function quoteIdentifier(string $identifier): string |
|
40 | { |
||
41 | return $this->quoter->identifier($identifier); |
||
42 | } |
||
43 | |||
44 | /** |
||
45 | 2254 | * @psalm-return non-empty-string |
|
46 | */ |
||
47 | public function compile( |
||
48 | QueryParameters $params, |
||
49 | string $prefix, |
||
50 | 2254 | FragmentInterface $fragment, |
|
51 | ): string { |
||
52 | 2254 | return $this->fragment( |
|
53 | $params, |
||
54 | 2254 | $this->quoter->withPrefix($prefix), |
|
55 | $fragment, |
||
56 | false, |
||
57 | ); |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | 1344 | * @psalm-return non-empty-string |
|
62 | */ |
||
63 | 1344 | public function hashLimit(QueryParameters $params, array $tokens): string |
|
64 | 66 | { |
|
65 | if ($tokens['limit'] !== null) { |
||
66 | $params->push(new Parameter($tokens['limit'])); |
||
67 | 1344 | } |
|
68 | 48 | ||
69 | if ($tokens['offset'] !== null) { |
||
70 | $params->push(new Parameter($tokens['offset'])); |
||
71 | 1344 | } |
|
72 | |||
73 | return '_' . ($tokens['limit'] === null) . '_' . ($tokens['offset'] === null); |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | 2254 | * @psalm-return non-empty-string |
|
78 | */ |
||
79 | protected function fragment( |
||
80 | QueryParameters $params, |
||
81 | Quoter $q, |
||
82 | FragmentInterface $fragment, |
||
83 | 2254 | bool $nestedQuery = true, |
|
84 | ): string { |
||
85 | 2254 | $tokens = $fragment->getTokens(); |
|
86 | 2254 | ||
87 | 662 | switch ($fragment->getType()) { |
|
88 | 16 | case self::FRAGMENT: |
|
89 | foreach ($tokens['parameters'] as $param) { |
||
90 | $params->push($param); |
||
91 | 662 | } |
|
92 | |||
93 | 1674 | return $tokens['fragment']; |
|
94 | 342 | ||
95 | 26 | case self::EXPRESSION: |
|
96 | foreach ($tokens['parameters'] as $param) { |
||
97 | $params->push($param); |
||
98 | 342 | } |
|
99 | |||
100 | 1670 | return $q->quote($tokens['expression']); |
|
101 | 272 | ||
102 | case self::JSON_EXPRESSION: |
||
103 | 1560 | foreach ($tokens['parameters'] as $param) { |
|
104 | 1440 | $params->push($param); |
|
105 | 112 | } |
|
106 | 72 | ||
107 | 72 | return $tokens['expression']; |
|
108 | 72 | ||
109 | case self::INSERT_QUERY: |
||
110 | return $this->insertQuery($params, $q, $tokens); |
||
111 | |||
112 | 112 | case self::SELECT_QUERY: |
|
113 | 112 | if ($nestedQuery) { |
|
114 | 112 | if ($fragment->getPrefix() !== null) { |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
115 | $q = $q->withPrefix( |
||
116 | $fragment->getPrefix(), |
||
117 | true, |
||
118 | 1432 | ); |
|
119 | } |
||
120 | 160 | ||
121 | 104 | return \sprintf( |
|
122 | '(%s)', |
||
123 | 56 | $this->selectQuery($params, $q, $tokens), |
|
124 | 56 | ); |
|
125 | } |
||
126 | |||
127 | return $this->selectQuery($params, $q, $tokens); |
||
128 | |||
129 | case self::SUBQUERY: |
||
130 | return $this->subQuery($params, $q, $tokens); |
||
131 | |||
132 | case self::UPDATE_QUERY: |
||
133 | return $this->updateQuery($params, $q, $tokens); |
||
134 | |||
135 | case self::DELETE_QUERY: |
||
136 | return $this->deleteQuery($params, $q, $tokens); |
||
137 | } |
||
138 | 236 | ||
139 | throw new CompilerException( |
||
140 | 236 | \sprintf( |
|
141 | 236 | 'Unknown fragment type %s', |
|
142 | 228 | $fragment->getType(), |
|
143 | ), |
||
144 | ); |
||
145 | 236 | } |
|
146 | 8 | ||
147 | 8 | /** |
|
148 | 8 | * @psalm-return non-empty-string |
|
149 | */ |
||
150 | protected function insertQuery(QueryParameters $params, Quoter $q, array $tokens): string |
||
151 | { |
||
152 | 228 | $values = []; |
|
153 | 228 | foreach ($tokens['values'] as $value) { |
|
154 | 228 | $values[] = $this->value($params, $q, $value); |
|
155 | 228 | } |
|
156 | 228 | ||
157 | if ($tokens['columns'] === []) { |
||
158 | return \sprintf( |
||
159 | 'INSERT INTO %s DEFAULT VALUES', |
||
160 | $this->name($params, $q, $tokens['table'], true), |
||
161 | ); |
||
162 | } |
||
163 | 990 | ||
164 | return \sprintf( |
||
165 | 'INSERT INTO %s (%s) VALUES %s', |
||
166 | 990 | $this->name($params, $q, $tokens['table'], true), |
|
167 | 990 | $this->columns($params, $q, $tokens['columns']), |
|
168 | 990 | \implode(', ', $values), |
|
169 | ); |
||
170 | 990 | } |
|
171 | 132 | ||
172 | /** |
||
173 | * @psalm-return non-empty-string |
||
174 | 990 | */ |
|
175 | 990 | protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string |
|
176 | 990 | { |
|
177 | 990 | // This statement(s) parts should be processed first to define set of table and column aliases |
|
178 | 990 | $tables = []; |
|
179 | 990 | foreach ($tokens['from'] as $table) { |
|
180 | 990 | $tables[] = $this->name($params, $q, $table, true); |
|
181 | 990 | } |
|
182 | 990 | foreach ($tokens['join'] as $join) { |
|
183 | 990 | $this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true); |
|
184 | 990 | } |
|
185 | 990 | ||
186 | 990 | return \sprintf( |
|
187 | "SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s%s%s", |
||
188 | $this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])), |
||
189 | $this->columns($params, $q, $tokens['columns']), |
||
190 | 1102 | \implode(', ', $tables), |
|
191 | $this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '), |
||
192 | 1102 | $this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])), |
|
193 | $this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '), |
||
194 | $this->optional("\nHAVING", $this->where($params, $q, $tokens['having'])), |
||
195 | 1440 | $this->optional("\n", $this->unions($params, $q, $tokens['union'])), |
|
196 | $this->optional("\n", $this->intersects($params, $q, $tokens['intersect'])), |
||
197 | 1440 | $this->optional("\n", $this->excepts($params, $q, $tokens['except'])), |
|
198 | 1440 | $this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])), |
|
199 | 202 | $this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])), |
|
200 | 202 | $this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : ''), |
|
201 | 202 | ); |
|
202 | 202 | } |
|
203 | |||
204 | protected function subQuery(QueryParameters $params, Quoter $q, array $tokens): string |
||
205 | 202 | { |
|
206 | 74 | return \sprintf('( %s ) AS %s', $this->selectQuery($params, $q, $tokens), $q->quote($tokens['alias'])); |
|
207 | } |
||
208 | 74 | ||
209 | protected function distinct(QueryParameters $params, Quoter $q, string|bool|array $distinct): string |
||
210 | { |
||
211 | 202 | return $distinct === false ? '' : 'DISTINCT'; |
|
0 ignored issues
–
show
|
|||
212 | 202 | } |
|
213 | 202 | ||
214 | protected function joins(QueryParameters $params, Quoter $q, array $joins): string |
||
215 | { |
||
216 | $statement = ''; |
||
217 | 1440 | foreach ($joins as $join) { |
|
218 | $statement .= \sprintf( |
||
219 | \str_contains($join['type'], 'JOIN') ? "\n%s %s" : "\n%s JOIN %s", |
||
220 | 1440 | $join['type'], |
|
221 | $this->nameWithAlias($params, $q, $join['outer'], $join['alias'], true), |
||
222 | 1440 | ); |
|
223 | 1440 | ||
224 | $statement .= $this->optional( |
||
225 | "\n ON", |
||
226 | 24 | $this->where($params, $q, $join['on']), |
|
227 | 24 | ); |
|
228 | 24 | } |
|
229 | |||
230 | 24 | return $statement; |
|
231 | } |
||
232 | 16 | ||
233 | protected function unions(QueryParameters $params, Quoter $q, array $unions): string |
||
234 | { |
||
235 | 16 | if ($unions === []) { |
|
236 | return ''; |
||
237 | } |
||
238 | |||
239 | 24 | $statement = ''; |
|
240 | foreach ($unions as $union) { |
||
241 | $select = $this->fragment($params, $q, $union[1]); |
||
242 | 1440 | ||
243 | if ($union[0] !== '') { |
||
244 | 1440 | //First key is union type, second united query (no need to share compiler) |
|
245 | 1440 | $statement .= "\nUNION {$union[0]}\n{$select}"; |
|
246 | 108 | } else { |
|
247 | //No extra space |
||
248 | 108 | $statement .= "\nUNION \n{$select}"; |
|
249 | } |
||
250 | } |
||
251 | |||
252 | 108 | return \ltrim($statement, "\n"); |
|
253 | } |
||
254 | |||
255 | 1440 | protected function intersects(QueryParameters $params, Quoter $q, array $intersects): string |
|
256 | { |
||
257 | if ($intersects === []) { |
||
258 | 1440 | return ''; |
|
259 | } |
||
260 | 1440 | ||
261 | 1440 | $statement = ''; |
|
262 | 80 | foreach ($intersects as $intersect) { |
|
263 | $select = $this->fragment($params, $q, $intersect[1]); |
||
264 | |||
265 | 1440 | if ($intersect[0] !== '') { |
|
266 | //First key is intersect type, second intersected query (no need to share compiler) |
||
267 | $statement .= "\nINTERSECT {$intersect[0]}\n{$select}"; |
||
268 | } else { |
||
269 | //No extra space |
||
270 | $statement .= "\nINTERSECT \n{$select}"; |
||
271 | } |
||
272 | } |
||
273 | |||
274 | return \ltrim($statement, "\n"); |
||
275 | 104 | } |
|
276 | |||
277 | protected function excepts(QueryParameters $params, Quoter $q, array $excepts): string |
||
278 | { |
||
279 | if ($excepts === []) { |
||
280 | 104 | return ''; |
|
281 | 104 | } |
|
282 | 104 | ||
283 | 104 | $statement = ''; |
|
284 | 104 | foreach ($excepts as $except) { |
|
285 | 104 | $select = $this->fragment($params, $q, $except[1]); |
|
286 | |||
287 | if ($except[0] !== '') { |
||
288 | //First key is except type, second excepted query (no need to share compiler) |
||
289 | 104 | $statement .= "\nEXCEPT {$except[0]}\n{$select}"; |
|
290 | 104 | } else { |
|
291 | 104 | //No extra space |
|
292 | 104 | $statement .= "\nEXCEPT \n{$select}"; |
|
293 | 104 | } |
|
294 | } |
||
295 | |||
296 | return \ltrim($statement, "\n"); |
||
297 | } |
||
298 | |||
299 | protected function orderBy(QueryParameters $params, Quoter $q, array $orderBy): string |
||
300 | 56 | { |
|
301 | $result = []; |
||
302 | foreach ($orderBy as $order) { |
||
303 | if (\is_string($order[0]) && $this->isJsonPath($order[0])) { |
||
304 | $order[0] = $this->compileJsonOrderBy($order[0]); |
||
305 | 56 | } |
|
306 | 56 | ||
307 | 56 | if ($order[1] === null) { |
|
308 | 56 | $result[] = $this->name($params, $q, $order[0]); |
|
309 | 56 | continue; |
|
310 | 56 | } |
|
311 | |||
312 | $direction = \strtoupper($order[1]); |
||
313 | |||
314 | \in_array($direction, static::ORDER_OPTIONS) or throw new CompilerException( |
||
315 | \sprintf( |
||
316 | 'Invalid sorting direction, only `%s` are allowed', |
||
317 | \implode('`, `', static::ORDER_OPTIONS), |
||
318 | 1670 | ), |
|
319 | ); |
||
320 | 1670 | ||
321 | 184 | $result[] = $this->name($params, $q, $order[0]) . ' ' . $direction; |
|
322 | } |
||
323 | |||
324 | 1670 | return \implode(', ', $result); |
|
325 | 8 | } |
|
326 | |||
327 | protected function groupBy(QueryParameters $params, Quoter $q, array $groupBy): string |
||
328 | 1670 | { |
|
329 | $result = []; |
||
330 | foreach ($groupBy as $identifier) { |
||
331 | $result[] = $this->name($params, $q, $identifier); |
||
332 | } |
||
333 | |||
334 | 1546 | return \implode(', ', $result); |
|
335 | } |
||
336 | |||
337 | 1546 | abstract protected function limit( |
|
338 | 1546 | QueryParameters $params, |
|
339 | 1546 | Quoter $q, |
|
340 | 1546 | ?int $limit = null, |
|
341 | ?int $offset = null, |
||
342 | ): string; |
||
343 | |||
344 | 1546 | protected function updateQuery( |
|
345 | QueryParameters $parameters, |
||
346 | Quoter $quoter, |
||
347 | array $tokens, |
||
348 | ): string { |
||
349 | $values = []; |
||
350 | 338 | foreach ($tokens['values'] as $column => $value) { |
|
351 | $values[] = \sprintf( |
||
352 | 338 | '%s = %s', |
|
353 | 16 | $this->name($parameters, $quoter, $column), |
|
354 | $this->value($parameters, $quoter, $value), |
||
355 | ); |
||
356 | 338 | } |
|
357 | 330 | ||
358 | return \sprintf( |
||
359 | "UPDATE %s\nSET %s%s", |
||
360 | 338 | $this->name($parameters, $quoter, $tokens['table'], true), |
|
361 | 256 | \trim(\implode(', ', $values)), |
|
362 | 256 | $this->optional("\nWHERE", $this->where($parameters, $quoter, $tokens['where'])), |
|
363 | 256 | ); |
|
364 | } |
||
365 | |||
366 | 256 | /** |
|
367 | * @psalm-return non-empty-string |
||
368 | */ |
||
369 | 338 | protected function deleteQuery( |
|
370 | QueryParameters $parameters, |
||
371 | 338 | Quoter $quoter, |
|
372 | array $tokens, |
||
373 | ): string { |
||
374 | 1560 | return \sprintf( |
|
375 | 'DELETE FROM %s%s', |
||
376 | 1560 | $this->name($parameters, $quoter, $tokens['table'], true), |
|
377 | 1496 | $this->optional( |
|
378 | "\nWHERE", |
||
379 | $this->where($parameters, $quoter, $tokens['where']), |
||
380 | 1194 | ), |
|
381 | ); |
||
382 | 1194 | } |
|
383 | 1194 | ||
384 | /** |
||
385 | 1194 | * @psalm-return non-empty-string |
|
386 | * @param mixed $name |
||
387 | */ |
||
388 | 1194 | protected function name(QueryParameters $params, Quoter $q, $name, bool $table = false): string |
|
389 | { |
||
390 | 1194 | if ($name instanceof FragmentInterface) { |
|
391 | return $this->fragment($params, $q, $name); |
||
392 | 480 | } |
|
393 | 480 | ||
394 | if ($name instanceof ParameterInterface) { |
||
395 | return $this->value($params, $q, $name); |
||
396 | } |
||
397 | |||
398 | return $q->quote($name, $table); |
||
399 | } |
||
400 | 1194 | ||
401 | 240 | /** |
|
402 | * @psalm-return non-empty-string |
||
403 | 240 | * @param mixed $name |
|
404 | */ |
||
405 | protected function nameWithAlias( |
||
406 | 240 | QueryParameters $params, |
|
407 | 240 | Quoter $q, |
|
408 | $name, |
||
409 | ?string $alias = null, |
||
410 | 1186 | bool $table = false, |
|
411 | 8 | ): string { |
|
412 | 8 | $quotedName = $this->name($params, $q, $name, $table); |
|
413 | 8 | ||
414 | if ($alias !== null) { |
||
415 | $q->registerAlias($alias, (string) $name); |
||
416 | |||
417 | 1186 | $quotedName .= ' AS ' . $this->name($params, $q, $alias); |
|
418 | 1186 | } |
|
419 | 1186 | ||
420 | 1186 | return $quotedName; |
|
421 | } |
||
422 | |||
423 | 1194 | /** |
|
424 | * @psalm-return non-empty-string |
||
425 | 1194 | */ |
|
426 | 8 | protected function columns(QueryParameters $params, Quoter $q, array $columns, int $maxLength = 180): string |
|
427 | { |
||
428 | // let's quote every identifier |
||
429 | 1186 | $columns = \array_map( |
|
430 | function ($column) use ($params, $q) { |
||
431 | return $this->name($params, $q, $column); |
||
432 | }, |
||
433 | $columns, |
||
434 | ); |
||
435 | 1186 | ||
436 | return \wordwrap(\implode(', ', $columns), $maxLength); |
||
437 | 1186 | } |
|
438 | 1186 | ||
439 | /** |
||
440 | 1186 | * @psalm-return non-empty-string |
|
441 | 16 | * @param mixed $value |
|
442 | 1170 | */ |
|
443 | protected function value(QueryParameters $params, Quoter $q, $value): string |
||
444 | { |
||
445 | if ($value instanceof FragmentInterface) { |
||
446 | 1186 | return $this->fragment($params, $q, $value); |
|
447 | 308 | } |
|
448 | |||
449 | if (!$value instanceof ParameterInterface) { |
||
450 | 1040 | $value = new Parameter($value); |
|
451 | } |
||
452 | |||
453 | if ($value->isArray()) { |
||
454 | 1040 | $values = []; |
|
455 | 1040 | foreach ($value->getValue() as $child) { |
|
456 | 50 | $values[] = $this->value($params, $q, $child); |
|
457 | } |
||
458 | 50 | ||
459 | return '(' . \implode(', ', $values) . ')'; |
||
460 | } |
||
461 | |||
462 | 50 | $params->push($value); |
|
463 | 50 | ||
464 | 1022 | return '?'; |
|
465 | 32 | } |
|
466 | 8 | ||
467 | 24 | protected function where(QueryParameters $params, Quoter $q, array $tokens): string |
|
468 | 8 | { |
|
469 | if ($tokens === []) { |
||
470 | return ''; |
||
471 | 32 | } |
|
472 | |||
473 | 990 | $statement = ''; |
|
474 | |||
475 | $activeGroup = true; |
||
476 | 1040 | foreach ($tokens as $condition) { |
|
477 | 64 | // OR/AND keyword |
|
478 | [$boolean, $context] = $condition; |
||
479 | |||
480 | 64 | // first condition in group/query, no any AND, OR required |
|
481 | if ($activeGroup) { |
||
482 | // first condition can have a `NOT` keyword (WHERE NOT ...) |
||
483 | 976 | if (\str_contains(\strtoupper($boolean), 'NOT')) { |
|
484 | $statement .= 'NOT'; |
||
485 | $statement .= ' '; |
||
486 | } |
||
487 | |||
488 | // next conditions require AND or OR |
||
489 | $activeGroup = false; |
||
490 | 1560 | } else { |
|
491 | $statement .= $boolean; |
||
492 | 1560 | $statement .= ' '; |
|
493 | 1496 | } |
|
494 | |||
495 | /* |
||
496 | 1292 | * When context is string it usually represent control keyword/syntax such as opening |
|
497 | 1236 | * or closing braces. |
|
498 | */ |
||
499 | if (\is_string($context)) { |
||
500 | 1292 | if ($context === '(') { |
|
501 | // new where group. |
||
502 | $activeGroup = true; |
||
503 | } |
||
504 | |||
505 | $statement .= $context; |
||
506 | continue; |
||
507 | } |
||
508 | |||
509 | if ($context instanceof FragmentInterface) { |
||
510 | $statement .= $this->fragment($params, $q, $context); |
||
511 | $statement .= ' '; |
||
512 | continue; |
||
513 | } |
||
514 | |||
515 | // identifier can be column name, expression or even query builder |
||
516 | $statement .= $this->name($params, $q, $context[0]); |
||
517 | $statement .= ' '; |
||
518 | $statement .= $this->condition($params, $q, $context); |
||
519 | $statement .= ' '; |
||
520 | } |
||
521 | |||
522 | $activeGroup and throw new CompilerException('Unable to build where statement, unclosed where group'); |
||
523 | |||
524 | if (\trim($statement, ' ()') === '') { |
||
525 | return ''; |
||
526 | } |
||
527 | |||
528 | return $statement; |
||
529 | } |
||
530 | |||
531 | /** |
||
532 | * @psalm-return non-empty-string |
||
533 | */ |
||
534 | protected function condition(QueryParameters $params, Quoter $q, array $context): string |
||
535 | { |
||
536 | $operator = $context[1]; |
||
537 | $value = $context[2]; |
||
538 | |||
539 | if ($operator instanceof FragmentInterface) { |
||
540 | $operator = $this->fragment($params, $q, $operator); |
||
541 | } elseif (!\is_string($operator)) { |
||
542 | throw new CompilerException('Invalid operator type, string or fragment is expected'); |
||
543 | } |
||
544 | |||
545 | if ($value instanceof FragmentInterface) { |
||
546 | return $operator . ' ' . $this->fragment($params, $q, $value); |
||
547 | } |
||
548 | |||
549 | if (!$value instanceof ParameterInterface) { |
||
550 | throw new CompilerException('Invalid value format, fragment or parameter is expected'); |
||
551 | } |
||
552 | |||
553 | $placeholder = '?'; |
||
554 | if ($value->isArray()) { |
||
555 | return $this->arrayToInOperator($params, $q, $value->getValue(), match (\strtoupper($operator)) { |
||
556 | 'IN', '=' => true, |
||
557 | 'NOT IN', '!=' => false, |
||
558 | default => throw CompilerException\UnexpectedOperatorException::sequence($operator), |
||
559 | }); |
||
560 | } |
||
561 | |||
562 | if ($value->isNull()) { |
||
563 | if ($operator === '=') { |
||
564 | $operator = 'IS'; |
||
565 | } elseif ($operator === '!=') { |
||
566 | $operator = 'IS NOT'; |
||
567 | } |
||
568 | |||
569 | $placeholder = 'NULL'; |
||
570 | } else { |
||
571 | $params->push($value); |
||
572 | } |
||
573 | |||
574 | if ($operator === 'BETWEEN' || $operator === 'NOT BETWEEN') { |
||
575 | $params->push($context[3]); |
||
576 | |||
577 | // possibly support between nested queries |
||
578 | return $operator . ' ? AND ?'; |
||
579 | } |
||
580 | |||
581 | return $operator . ' ' . $placeholder; |
||
582 | } |
||
583 | |||
584 | /** |
||
585 | * Combine expression with prefix/postfix (usually SQL keyword) but only if expression is not |
||
586 | * empty. |
||
587 | */ |
||
588 | protected function optional(string $prefix, string $expression, string $postfix = ''): string |
||
589 | { |
||
590 | if ($expression === '') { |
||
591 | return ''; |
||
592 | } |
||
593 | |||
594 | if ($prefix !== "\n" && $prefix !== ' ') { |
||
595 | $prefix .= ' '; |
||
596 | } |
||
597 | |||
598 | return $prefix . $expression . $postfix; |
||
599 | } |
||
600 | |||
601 | protected function isJsonPath(string $column): bool |
||
602 | { |
||
603 | return \str_contains($column, '->'); |
||
604 | } |
||
605 | |||
606 | /** |
||
607 | * Each driver must override this method and implement sorting by JSON column. |
||
608 | */ |
||
609 | protected function compileJsonOrderBy(string $path): string|FragmentInterface |
||
610 | { |
||
611 | return $path; |
||
612 | } |
||
613 | |||
614 | private function arrayToInOperator(QueryParameters $params, Quoter $q, array $values, bool $in): string |
||
615 | { |
||
616 | $operator = $in ? 'IN' : 'NOT IN'; |
||
617 | |||
618 | $placeholders = $simpleParams = []; |
||
619 | foreach ($values as $value) { |
||
620 | if ($value instanceof FragmentInterface) { |
||
621 | $placeholders[] = $this->fragment($params, $q, $value); |
||
622 | } else { |
||
623 | $placeholders[] = '?'; |
||
624 | $simpleParams[] = $value; |
||
625 | } |
||
626 | } |
||
627 | if ($simpleParams !== []) { |
||
628 | $params->push(new Parameter($simpleParams)); |
||
629 | } |
||
630 | |||
631 | return \sprintf('%s(%s)', $operator, \implode(',', $placeholders)); |
||
632 | } |
||
633 | } |
||
634 |