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

nextras / dbal / 19193970809

08 Nov 2025 01:56PM UTC coverage: 86.873% (+0.3%) from 86.622%
19193970809

push

github

web-flow
Merge pull request #313 from nextras/php8.5

build for PHP 8.5

6 of 7 new or added lines in 1 file covered. (85.71%)

1992 of 2293 relevant lines covered (86.87%)

5.09 hits per line

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

96.44
/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)
6✔
69
        {
70
                $this->modifierResolvers = new SplObjectStorage();
6✔
71
        }
6✔
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, '[]?');
6✔
80
                if (isset($this->modifiers[$baseModifier])) {
6✔
81
                        throw new InvalidArgumentException("Cannot override core modifier '$baseModifier'.");
6✔
82
                }
83

84
                $this->customModifiers[$modifier] = $callback;
6✔
85
        }
6✔
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->offsetSet(object: $resolver);
6✔
94
        }
6✔
95

96

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

105

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

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

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

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

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

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

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

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

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

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

159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

428

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

438

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

445

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

452

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

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

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

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

477

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

491
                return implode(', ', $values);
6✔
492
        }
493

494

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

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

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

524

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

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

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

544

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

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

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

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

622
                        $operands[] = $operand;
6✔
623
                }
624

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

628

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

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

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

699

700
        protected function getVariableTypeName(mixed $value): float|string
701
        {
702
                return is_object($value)
6✔
703
                        ? $value::class
6✔
704
                        : (is_float($value) && !is_finite($value)
6✔
705
                                ? (is_nan($value) ? "NAN" : $value)
6✔
706
                                : gettype($value));
6✔
707
        }
708

709

710
        protected function identifierToSql(string $key): string
711
        {
712
                return $this->identifiers[$key] ??
6✔
713
                        ($this->identifiers[$key] = // = intentionally
6✔
714
                                str_ends_with($key, '.*')
6✔
715
                                        ? $this->platform->formatIdentifier(substr($key, 0, -2)) . '.*'
6✔
716
                                        : $this->platform->formatIdentifier($key)
6✔
717
                        );
718
        }
719
}
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