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

nextras / dbal / 18675330340

21 Oct 2025 06:46AM UTC coverage: 86.655% (+0.006%) from 86.649%
18675330340

Pull #289

github

web-flow
Merge 886b0ebf0 into a89576c0a
Pull Request #289: Add "sslrootcert" to knownKeys in PgsqlDriver

1948 of 2248 relevant lines covered (86.65%)

4.31 hits per line

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

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

84
                $this->customModifiers[$modifier] = $callback;
5✔
85
        }
5✔
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);
5✔
94
        }
5✔
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;
5✔
112
                $fragments = [];
5✔
113

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

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

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

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

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

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

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

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

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

159

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

428

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

438

439
        protected function throwInvalidValueTypeException(string $type, mixed $value, string $expectedType): never
440
        {
441
                $actualType = $this->getVariableTypeName($value);
5✔
442
                throw new InvalidArgumentException("Modifier %$type expects value to be $expectedType, $actualType given.");
5✔
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);
5✔
449
                throw new InvalidArgumentException("Modifier %$type does not allow $valueLabel value, use modifier %$hint instead.");
5✔
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);
5✔
459
                $wrapped = true;
5✔
460

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

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

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

477

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

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

494

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

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

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

524

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

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

541
                return '(' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
5✔
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);
5✔
577
                if ($totalCount === 0) {
5✔
578
                        return '1=1';
5✔
579
                }
580

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

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

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

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

625
                return implode($type === 'and' ? ' AND ' : ' OR ', $operands);
5✔
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)) {
5✔
658
                        $sqls = [];
5✔
659
                        foreach ($values as $value) {
5✔
660
                                $sqls[] = $this->processWhere('and', $value);
5✔
661
                        }
662
                        return '(' . implode(') OR (', $sqls) . ')';
5✔
663
                }
664

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

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

699

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

705

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

© 2026 Coveralls, Inc