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