• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nextras / dbal / 13619972298

02 Mar 2025 10:33PM UTC coverage: 86.649% (-0.1%) from 86.756%
13619972298

push

github

web-flow
implement LocalDate %ld modifier (#283)

7 of 12 new or added lines in 5 files covered. (58.33%)

1947 of 2247 relevant lines covered (86.65%)

3.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.39
/src/SqlProcessor.php
1
<?php declare(strict_types = 1);
2

3
namespace Nextras\Dbal;
4

5

6
use DateInterval;
7
use DateTime;
8
use DateTimeImmutable;
9
use Nextras\Dbal\Exception\InvalidArgumentException;
10
use Nextras\Dbal\Platforms\Data\Fqn;
11
use Nextras\Dbal\Platforms\IPlatform;
12
use Nextras\Dbal\Utils\StrictObjectTrait;
13
use SplObjectStorage;
14

15

16
class SqlProcessor
17
{
18
        use StrictObjectTrait;
19

20

21
        /**
22
         * Modifiers definition in form of array (name => [supports ?, supports [], expected type description]).
23
         * @var array<string, array{bool, bool, string}>
24
         */
25
        protected $modifiers = [
26
                // expressions
27
                's' => [true, true, 'string'],
28
                'json' => [true, true, 'pretty much anything'],
29
                'i' => [true, true, 'int'],
30
                'f' => [true, true, '(finite) float'],
31
                'b' => [true, true, 'bool'],
32
                'dt' => [true, true, 'DateTimeInterface'],
33
                'dts' => [true, true, 'DateTimeInterface'], // @deprecated use ldt
34
                'ldt' => [true, true, 'DateTimeInterface'],
35
                'ld' => [true, true, 'DateTimeInterface|string(YYYY-MM-DD)'],
36
                'di' => [true, true, 'DateInterval'],
37
                'blob' => [true, true, 'blob string'],
38
                '_like' => [true, false, 'string'],
39
                'like_' => [true, false, 'string'],
40
                '_like_' => [true, false, 'string'],
41
                'any' => [false, false, 'pretty much anything'],
42
                'and' => [false, false, 'array'],
43
                'or' => [false, false, 'array'],
44
                'multiOr' => [false, false, 'array'],
45

46
                // SQL constructs
47
                'table' => [false, true, 'string|array'],
48
                'column' => [false, true, 'string'],
49
                'values' => [false, true, 'array'],
50
                'set' => [false, false, 'array'],
51
                'raw' => [false, false, 'string'],
52
                'ex' => [false, false, 'array'],
53
        ];
54

55
        /**
56
         * Modifiers storage as array(modifier name => callable)
57
         * @var array<string, callable(SqlProcessor, mixed, string): mixed>
58
         */
59
        protected $customModifiers = [];
60

61
        /** @var SplObjectStorage<ISqlProcessorModifierResolver, never> */
62
        protected SplObjectStorage $modifierResolvers;
63

64
        /** @var array<string, string> */
65
        private ?array $identifiers = null;
66

67

68
        public function __construct(private readonly IPlatform $platform)
4✔
69
        {
70
                $this->modifierResolvers = new SplObjectStorage();
4✔
71
        }
4✔
72

73

74
        /**
75
         * @param callable(SqlProcessor, mixed $value, string $modifier): mixed $callback
76
         */
77
        public function setCustomModifier(string $modifier, callable $callback): void
78
        {
79
                $baseModifier = trim($modifier, '[]?');
4✔
80
                if (isset($this->modifiers[$baseModifier])) {
4✔
81
                        throw new InvalidArgumentException("Cannot override core modifier '$baseModifier'.");
4✔
82
                }
83

84
                $this->customModifiers[$modifier] = $callback;
4✔
85
        }
4✔
86

87

88
        /**
89
         * Adds a modifier resolver for any unspecified type (either implicit or explicit `%any` modifier).
90
         */
91
        public function addModifierResolver(ISqlProcessorModifierResolver $resolver): void
92
        {
93
                $this->modifierResolvers->attach($resolver);
4✔
94
        }
4✔
95

96

97
        /**
98
         * Removes modifier resolver.
99
         */
100
        public function removeModifierResolver(ISqlProcessorModifierResolver $resolver): void
101
        {
102
                $this->modifierResolvers->detach($resolver);
×
103
        }
×
104

105

106
        /**
107
         * @param mixed[] $args
108
         */
109
        public function process(array $args): string
110
        {
111
                $last = count($args) - 1;
4✔
112
                $fragments = [];
4✔
113

114
                for ($i = 0, $j = 0; $j <= $last; $j++) {
4✔
115
                        if (!is_string($args[$j])) {
4✔
116
                                throw new InvalidArgumentException($j === 0
4✔
117
                                        ? 'Query fragment must be string.'
4✔
118
                                        : "Redundant query parameter or missing modifier in query fragment '$args[$i]'.",
4✔
119
                                );
120
                        }
121

122
                        $i = $j;
4✔
123
                        $fragments[] = preg_replace_callback(
4✔
124
                                '#%((?:\.\.\.)?+\??+\w++(?:\[]){0,2}+)|(%%)|(\[\[)|(]])|\[(.+?)]#S', // %modifier | %% | [[ | ]] | [identifier]
4✔
125
                                function($matches) use ($args, &$j, $last): string {
4✔
126
                                        if ($matches[1] !== '') {
4✔
127
                                                if ($j === $last) {
4✔
128
                                                        throw new InvalidArgumentException("Missing query parameter for modifier $matches[0].");
4✔
129
                                                }
130
                                                return $this->processModifier($matches[1], $args[++$j]);
4✔
131

132
                                        } elseif ($matches[2] !== '') {
4✔
133
                                                return '%';
4✔
134

135
                                        } elseif ($matches[3] !== '') {
4✔
136
                                                return '[';
4✔
137

138
                                        } elseif ($matches[4] !== '') {
4✔
139
                                                return ']';
4✔
140

141
                                        } elseif (!ctype_digit($matches[5])) {
4✔
142
                                                return $this->identifierToSql($matches[5]);
4✔
143

144
                                        } else {
145
                                                return "[$matches[5]]";
4✔
146
                                        }
147
                                },
4✔
148
                                (string) $args[$i],
4✔
149
                        );
150

151
                        if ($i === $j && $j !== $last) {
4✔
152
                                throw new InvalidArgumentException("Redundant query parameter or missing modifier in query fragment '$args[$i]'.");
4✔
153
                        }
154
                }
155

156
                return implode(' ', $fragments);
4✔
157
        }
158

159

160
        public function processModifier(string $type, mixed $value): string
161
        {
162
                if ($value instanceof \BackedEnum) {
4✔
163
                        $value = $value->value;
4✔
164
                }
165

166
                if ($type === 'any') {
4✔
167
                        $type = $this->detectType($value) ?? 'any';
4✔
168
                }
169

170
                switch (gettype($value)) {
4✔
171
                        case 'string':
4✔
172
                                switch ($type) {
173
                                        case 'any':
4✔
174
                                        case 's':
4✔
175
                                        case '?s':
4✔
176
                                                return $this->platform->formatString($value);
4✔
177

178
                                        case 'json':
4✔
179
                                        case '?json':
4✔
180
                                                return $this->platform->formatJson($value);
4✔
181

182
                                        case 'i':
4✔
183
                                        case '?i':
4✔
184
                                                if (preg_match('#^-?[1-9][0-9]*+\z#', $value) !== 1) {
4✔
185
                                                        break;
4✔
186
                                                }
187
                                                return $value;
4✔
188

189
                                        case 'ld':
4✔
190
                                        case '?ld':
4✔
191
                                                if (preg_match('#^\d{4}-\d{2}-\d{2}$#', $value) !== 1) {
4✔
192
                                                        break;
4✔
193
                                                }
194
                                                return $this->platform->formatString($value);
4✔
195

196
                                        case '_like':
4✔
197
                                                return $this->platform->formatStringLike($value, -1);
4✔
198
                                        case 'like_':
4✔
199
                                                return $this->platform->formatStringLike($value, 1);
4✔
200
                                        case '_like_':
4✔
201
                                                return $this->platform->formatStringLike($value, 0);
4✔
202

203
                                        /** @noinspection PhpMissingBreakStatementInspection */
204
                                        case 'column':
4✔
205
                                                if ($value === '*') {
4✔
206
                                                        return '*';
×
207
                                                }
208
                                        // intentional pass-through
209
                                        case 'table':
4✔
210
                                                return $this->identifierToSql($value);
4✔
211

212
                                        case 'blob':
4✔
213
                                                return $this->platform->formatBlob($value);
4✔
214

215
                                        case 'raw':
4✔
216
                                                return $value;
4✔
217
                                }
218

219
                                break;
4✔
220
                        case 'integer':
4✔
221
                                switch ($type) {
222
                                        case 'any':
4✔
223
                                        case 'i':
4✔
224
                                        case '?i':
4✔
225
                                                return (string) $value;
4✔
226

227
                                        case 'json':
4✔
228
                                        case '?json':
4✔
229
                                                return $this->platform->formatJson($value);
4✔
230
                                }
231

232
                                break;
4✔
233
                        case 'double':
4✔
234
                                if (is_finite($value)) { // database can not handle INF and NAN
4✔
235
                                        switch ($type) {
236
                                                case 'any':
4✔
237
                                                case 'f':
4✔
238
                                                case '?f':
4✔
239
                                                        $tmp = json_encode($value, JSON_THROW_ON_ERROR);
4✔
240
                                                        return $tmp . (!str_contains($tmp, '.') ? '.0' : '');
4✔
241

242
                                                case 'json':
4✔
243
                                                case '?json':
4✔
244
                                                        return $this->platform->formatJson($value);
4✔
245
                                        }
246
                                }
247

248
                                break;
4✔
249
                        case 'boolean':
4✔
250
                                switch ($type) {
251
                                        case 'any':
4✔
252
                                        case 'b':
4✔
253
                                        case '?b':
4✔
254
                                                return $this->platform->formatBool($value);
4✔
255

256
                                        case 'json':
4✔
257
                                        case '?json':
4✔
258
                                                return $this->platform->formatJson($value);
4✔
259
                                }
260

261
                                break;
4✔
262
                        case 'NULL':
4✔
263
                                switch ($type) {
264
                                        case 'any':
4✔
265
                                        case '?s':
4✔
266
                                        case '?i':
4✔
267
                                        case '?f':
4✔
268
                                        case '?b':
4✔
269
                                        case '?dt':
4✔
270
                                        case '?dts':
4✔
271
                                        case '?ldt':
4✔
272
                                        case '?di':
4✔
273
                                        case '?blob':
4✔
274
                                        case '?json':
4✔
275
                                                return 'NULL';
4✔
276
                                }
277

278
                                break;
4✔
279
                        case 'object':
4✔
280
                                if ($type === 'json' || $type === '?json') {
4✔
281
                                        return $this->platform->formatJson($value);
4✔
282
                                }
283

284
                                if ($value instanceof DateTimeImmutable || $value instanceof DateTime) {
4✔
285
                                        switch ($type) {
286
                                                case 'any':
4✔
287
                                                case 'dt':
4✔
288
                                                case '?dt':
4✔
289
                                                        return $this->platform->formatDateTime($value);
4✔
290

291
                                                case 'dts':
4✔
292
                                                case '?dts':
4✔
293
                                                case 'ldt':
4✔
294
                                                case '?ldt':
4✔
295
                                                        return $this->platform->formatLocalDateTime($value);
4✔
296

297
                                                case 'ld':
4✔
NEW
298
                                                case '?ld':
×
299
                                                        return $this->platform->formatLocalDate($value);
4✔
300
                                        }
301

302
                                } elseif ($value instanceof DateInterval) {
4✔
303
                                        switch ($type) {
304
                                                case 'any':
4✔
305
                                                case 'di':
×
306
                                                case '?di':
×
307
                                                        return $this->platform->formatDateInterval($value);
4✔
308
                                        }
309

310
                                } elseif ($value instanceof Fqn) {
4✔
311
                                        switch ($type) {
312
                                                case 'column':
4✔
313
                                                case 'table':
4✔
314
                                                        $schema = $this->identifierToSql($value->schema);
4✔
315
                                                        $table = $this->identifierToSql($value->name);
4✔
316
                                                        return "$schema.$table";
4✔
317
                                        }
318

319
                                } elseif (method_exists($value, '__toString')) {
4✔
320
                                        switch ($type) {
321
                                                case 'any':
4✔
322
                                                case 's':
4✔
323
                                                case '?s':
4✔
324
                                                        return $this->platform->formatString((string) $value);
4✔
325

326
                                                case '_like':
4✔
327
                                                        return $this->platform->formatStringLike((string) $value, -1);
×
328
                                                case 'like_':
4✔
329
                                                        return $this->platform->formatStringLike((string) $value, 1);
×
330
                                                case '_like_':
4✔
331
                                                        return $this->platform->formatStringLike((string) $value, 0);
×
332
                                        }
333
                                }
334

335
                                break;
4✔
336
                        case 'array':
4✔
337
                                switch ($type) {
338
                                        // micro-optimizations
339
                                        case 'any':
4✔
340
                                                return $this->processArray("any[]", $value);
4✔
341

342
                                        case 'i[]':
4✔
343
                                                foreach ($value as $v) {
4✔
344
                                                        if (!is_int($v)) break 2; // fallback to processArray
4✔
345
                                                }
346
                                                return '(' . implode(', ', $value) . ')';
4✔
347

348
                                        case 's[]':
4✔
349
                                                foreach ($value as &$subValue) {
4✔
350
                                                        if (!is_string($subValue)) break 2; // fallback to processArray
4✔
351
                                                        $subValue = $this->platform->formatString($subValue);
4✔
352
                                                }
353
                                                return '(' . implode(', ', $value) . ')';
4✔
354

355
                                        case 'json':
4✔
356
                                        case '?json':
4✔
357
                                                return $this->platform->formatJson($value);
4✔
358

359
                                        // normal
360
                                        case 'column[]':
4✔
361
                                        case '...column[]':
4✔
362
                                        case 'table[]':
4✔
363
                                        case '...table[]':
4✔
364
                                                $subType = substr($type, 0, -2);
4✔
365
                                                foreach ($value as &$subValue) {
4✔
366
                                                        $subValue = $this->processModifier($subType, $subValue);
4✔
367
                                                }
368
                                                return implode(', ', $value);
4✔
369

370
                                        case 'and':
4✔
371
                                        case 'or':
4✔
372
                                                return $this->processWhere($type, $value);
4✔
373

374
                                        case 'multiOr':
4✔
375
                                                return $this->processMultiColumnOr($value);
4✔
376

377
                                        case 'values':
4✔
378
                                                return $this->processValues($value);
4✔
379

380
                                        case 'values[]':
4✔
381
                                                return $this->processMultiValues($value);
4✔
382

383
                                        case 'set':
4✔
384
                                                return $this->processSet($value);
4✔
385

386
                                        case 'ex':
4✔
387
                                                return $this->process($value);
4✔
388
                                }
389

390
                                if (str_ends_with($type, ']')) {
4✔
391
                                        $baseType = trim(trim($type, '.'), '[]?');
4✔
392
                                        if (isset($this->modifiers[$baseType]) && $this->modifiers[$baseType][1]) {
4✔
393
                                                return $this->processArray($type, $value);
4✔
394
                                        }
395
                                }
396
                }
397

398
                $baseType = trim(trim($type, '.'), '[]?');
4✔
399

400
                if (isset($this->customModifiers[$baseType])) {
4✔
401
                        return $this->customModifiers[$baseType]($this, $value, $type);
4✔
402
                }
403

404
                $typeNullable = $type[0] === '?';
4✔
405
                $typeArray = str_ends_with($type, '[]');
4✔
406

407
                if (!isset($this->modifiers[$baseType])) {
4✔
408
                        throw new InvalidArgumentException("Unknown modifier %$type.");
4✔
409

410
                } elseif (($typeNullable && !$this->modifiers[$baseType][0]) || ($typeArray && !$this->modifiers[$baseType][1])) {
4✔
411
                        throw new InvalidArgumentException("Modifier %$baseType does not have %$type variant.");
4✔
412

413
                } elseif ($typeArray) {
4✔
414
                        $this->throwInvalidValueTypeException($type, $value, 'array');
4✔
415

416
                } elseif ($value === null && !$typeNullable && $this->modifiers[$baseType][0]) {
4✔
417
                        $this->throwWrongModifierException($type, $value, "?$type");
4✔
418

419
                } elseif (is_array($value) && $this->modifiers[$baseType][1]) {
4✔
420
                        $this->throwWrongModifierException($type, $value, "{$type}[]");
4✔
421

422
                } else {
423
                        $this->throwInvalidValueTypeException($type, $value, $this->modifiers[$baseType][2]);
4✔
424
                }
425
        }
×
426

427

428
        protected function detectType(mixed $value): ?string
429
        {
430
                foreach ($this->modifierResolvers as $modifierResolver) {
4✔
431
                        $resolved = $modifierResolver->resolve($value);
4✔
432
                        if ($resolved !== null) return $resolved;
4✔
433
                }
434
                return null;
4✔
435
        }
436

437

438
        protected function throwInvalidValueTypeException(string $type, mixed $value, string $expectedType): never
439
        {
440
                $actualType = $this->getVariableTypeName($value);
4✔
441
                throw new InvalidArgumentException("Modifier %$type expects value to be $expectedType, $actualType given.");
4✔
442
        }
443

444

445
        protected function throwWrongModifierException(string $type, mixed $value, string $hint): never
446
        {
447
                $valueLabel = is_scalar($value) ? var_export($value, true) : gettype($value);
4✔
448
                throw new InvalidArgumentException("Modifier %$type does not allow $valueLabel value, use modifier %$hint instead.");
4✔
449
        }
450

451

452
        /**
453
         * @param array<mixed> $value
454
         */
455
        protected function processArray(string $type, array $value): string
456
        {
457
                $subType = substr($type, 0, -2);
4✔
458
                $wrapped = true;
4✔
459

460
                if (str_starts_with($subType, '...')) {
4✔
461
                        $subType = substr($subType, 3);
4✔
462
                        $wrapped = false;
4✔
463
                }
464

465
                foreach ($value as &$subValue) {
4✔
466
                        $subValue = $this->processModifier($subType, $subValue);
4✔
467
                }
468

469
                if ($wrapped) {
4✔
470
                        return '(' . implode(', ', $value) . ')';
4✔
471
                } else {
472
                        return implode(', ', $value);
4✔
473
                }
474
        }
475

476

477
        /**
478
         * @param array<string, mixed> $value
479
         */
480
        protected function processSet(array $value): string
481
        {
482
                $values = [];
4✔
483
                foreach ($value as $_key => $val) {
4✔
484
                        $key = explode('%', $_key, 2);
4✔
485
                        $column = $this->identifierToSql($key[0]);
4✔
486
                        $expr = $this->processModifier($key[1] ?? 'any', $val);
4✔
487
                        $values[] = "$column = $expr";
4✔
488
                }
489

490
                return implode(', ', $values);
4✔
491
        }
492

493

494
        /**
495
         * @param array<string, mixed> $value
496
         */
497
        protected function processMultiValues(array $value): string
498
        {
499
                if (count($value) === 0) {
4✔
500
                        throw new InvalidArgumentException('Modifier %values[] must contain at least one array element.');
4✔
501
                }
502

503
                $keys = $values = [];
4✔
504
                foreach (array_keys(reset($value)) as $key) {
4✔
505
                        $keys[] = $this->identifierToSql(explode('%', (string) $key, 2)[0]);
4✔
506
                }
507
                foreach ($value as $subValue) {
4✔
508
                        if (!is_array($subValue) || count($subValue) === 0) {
4✔
509
                                $values[] = '(' . str_repeat('DEFAULT, ', max(count($keys) - 1, 0)) . 'DEFAULT)';
4✔
510
                        } else {
511
                                $subValues = [];
4✔
512
                                foreach ($subValue as $_key => $val) {
4✔
513
                                        $key = explode('%', (string) $_key, 2);
4✔
514
                                        $subValues[] = $this->processModifier($key[1] ?? 'any', $val);
4✔
515
                                }
516
                                $values[] = '(' . implode(', ', $subValues) . ')';
4✔
517
                        }
518
                }
519

520
                return (count($keys) > 0 ? '(' . implode(', ', $keys) . ') ' : '') . 'VALUES ' . implode(', ', $values);
4✔
521
        }
522

523

524
        /**
525
         * @param array<string, mixed> $value
526
         */
527
        private function processValues(array $value): string
528
        {
529
                if (count($value) === 0) {
4✔
530
                        return 'VALUES (DEFAULT)';
4✔
531
                }
532

533
                $keys = $values = [];
4✔
534
                foreach ($value as $_key => $val) {
4✔
535
                        $key = explode('%', $_key, 2);
4✔
536
                        $keys[] = $this->identifierToSql($key[0]);
4✔
537
                        $values[] = $this->processModifier($key[1] ?? 'any', $val);
4✔
538
                }
539

540
                return '(' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
4✔
541
        }
542

543

544
        /**
545
         * Handles multiple condition formats for AND and OR operators.
546
         *
547
         * Key-based:
548
         * ```
549
         * $connection->query('%or', [
550
         *     'city' => 'Winterfell',
551
         *     'age%i[]' => [23, 25],
552
         * ]);
553
         * ```
554
         *
555
         * Auto-expanding:
556
         * ```
557
         * $connection->query('%or', [
558
         *     'city' => 'Winterfell',
559
         *     ['[age] IN %i[]', [23, 25]],
560
         * ]);
561
         * ```
562
         *
563
         * Fqn instsance-based:
564
         * ```
565
         * $connection->query('%or', [
566
         *     [new Fqn(schema: '', name: 'city'), 'Winterfell'],
567
         *     [new Fqn(schema: '', name: 'age'), [23, 25], '%i[]'],
568
         * ]);
569
         * ```
570
         *
571
         * @param array<int|string, mixed> $value
572
         */
573
        private function processWhere(string $type, array $value): string
574
        {
575
                $totalCount = \count($value);
4✔
576
                if ($totalCount === 0) {
4✔
577
                        return '1=1';
4✔
578
                }
579

580
                $operands = [];
4✔
581
                foreach ($value as $_key => $subValue) {
4✔
582
                        if (is_int($_key)) {
4✔
583
                                if (!is_array($subValue)) {
4✔
584
                                        $subValueType = $this->getVariableTypeName($subValue);
4✔
585
                                        throw new InvalidArgumentException("Modifier %$type requires items with numeric index to be array, $subValueType given.");
4✔
586
                                }
587

588
                                if (count($subValue) > 0 && ($subValue[0] ?? null) instanceof Fqn) {
4✔
589
                                        $column = $this->processModifier('column', $subValue[0]);
4✔
590
                                        $subType = substr($subValue[2] ?? '%any', 1);
4✔
591
                                        if ($subValue[1] === null) {
4✔
592
                                                $op = ' IS ';
×
593
                                        } elseif (is_array($subValue[1])) {
4✔
594
                                                $op = ' IN ';
×
595
                                        } else {
596
                                                $op = ' = ';
4✔
597
                                        }
598
                                        $operand = $column . $op . $this->processModifier($subType, $subValue[1]);
4✔
599
                                } else {
600
                                        if ($totalCount === 1) {
4✔
601
                                                $operand = $this->process($subValue);
4✔
602
                                        } else {
603
                                                $operand = '(' . $this->process($subValue) . ')';
4✔
604
                                        }
605
                                }
606

607
                        } else {
608
                                $key = explode('%', $_key, 2);
4✔
609
                                $column = $this->identifierToSql($key[0]);
4✔
610
                                $subType = $key[1] ?? 'any';
4✔
611
                                if ($subValue === null) {
4✔
612
                                        $op = ' IS ';
4✔
613
                                } elseif (is_array($subValue) && $subType !== 'ex') {
4✔
614
                                        $op = ' IN ';
4✔
615
                                } else {
616
                                        $op = ' = ';
4✔
617
                                }
618
                                $operand = $column . $op . $this->processModifier($subType, $subValue);
4✔
619
                        }
620

621
                        $operands[] = $operand;
4✔
622
                }
623

624
                return implode($type === 'and' ? ' AND ' : ' OR ', $operands);
4✔
625
        }
626

627

628
        /**
629
         * Handles multi-column conditions with multiple paired values.
630
         *
631
         * The implementation considers database support and if not available, delegates to {@see processWhere} and joins
632
         * the resulting SQLs with OR operator.
633
         *
634
         * Key-based:
635
         * ```
636
         * $connection->query('%multiOr', [
637
         *     ['tag_id%i' => 1, 'book_id' => 23],
638
         *     ['tag_id%i' => 4, 'book_id' => 12],
639
         *     ['tag_id%i' => 9, 'book_id' => 83],
640
         * ]);
641
         * ```
642
         *
643
         * Fqn instance-based:
644
         * ```
645
         * $connection->query('%multiOr', [
646
         *     [[new Fqn('tbl', 'tag_id'), 1, '%i'], [new Fqn('tbl', 'book_id'), 23]],
647
         *     [[new Fqn('tbl', 'tag_id'), 4, '%i'], [new Fqn('tbl', 'book_id'), 12]],
648
         *     [[new Fqn('tbl', 'tag_id'), 9, '%i'], [new Fqn('tbl', 'book_id'), 83]],
649
         * ]);
650
         * ```
651
         *
652
         * @param array<string, mixed>|list<list<array{Fqn, mixed, 2?: string}>> $values
653
         */
654
        private function processMultiColumnOr(array $values): string
655
        {
656
                if (!$this->platform->isSupported(IPlatform::SUPPORT_MULTI_COLUMN_IN)) {
4✔
657
                        $sqls = [];
4✔
658
                        foreach ($values as $value) {
4✔
659
                                $sqls[] = $this->processWhere('and', $value);
4✔
660
                        }
661
                        return '(' . implode(') OR (', $sqls) . ')';
4✔
662
                }
663

664
                // Detect Fqn instance-based variant
665
                $isFqnBased = ($values[0][0][0] ?? null) instanceof Fqn;
4✔
666
                if ($isFqnBased) {
4✔
667
                        $keys = [];
4✔
668
                        foreach ($values[0] as $triple) {
4✔
669
                                $keys[] = $this->processModifier('column', $triple[0]);
4✔
670
                        }
671
                        foreach ($values as &$subValue) {
4✔
672
                                foreach ($subValue as &$subSubValue) {
4✔
673
                                        $type = substr($subSubValue[2] ?? '%any', 1);
4✔
674
                                        $subSubValue = $this->processModifier($type, $subSubValue[1]);
4✔
675
                                }
676
                                $subValue = '(' . implode(', ', $subValue) . ')';
4✔
677
                        }
678
                        return '(' . implode(', ', $keys) . ') IN (' . implode(', ', $values) . ')';
4✔
679
                }
680

681
                $keys = [];
4✔
682
                $modifiers = [];
4✔
683
                foreach (array_keys(reset($values)) as $key) {
4✔
684
                        $exploded = explode('%', (string) $key, 2);
4✔
685
                        $keys[] = $this->identifierToSql($exploded[0]);
4✔
686
                        $modifiers[] = $exploded[1] ?? 'any';
4✔
687
                }
688
                foreach ($values as &$subValue) {
4✔
689
                        $i = 0;
4✔
690
                        foreach ($subValue as &$subSubValue) {
4✔
691
                                $subSubValue = $this->processModifier($modifiers[$i++], $subSubValue);
4✔
692
                        }
693
                        $subValue = '(' . implode(', ', $subValue) . ')';
4✔
694
                }
695
                return '(' . implode(', ', $keys) . ') IN (' . implode(', ', $values) . ')';
4✔
696
        }
697

698

699
        protected function getVariableTypeName(mixed $value): float|string
700
        {
701
                return is_object($value) ? $value::class : (is_float($value) && !is_finite($value) ? $value : gettype($value));
4✔
702
        }
703

704

705
        protected function identifierToSql(string $key): string
706
        {
707
                return $this->identifiers[$key] ??
4✔
708
                        ($this->identifiers[$key] = // = intentionally
4✔
709
                                str_ends_with($key, '.*')
4✔
710
                                        ? $this->platform->formatIdentifier(substr($key, 0, -2)) . '.*'
4✔
711
                                        : $this->platform->formatIdentifier($key)
4✔
712
                        );
713
        }
714
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc