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

liqueurdetoile / cakephp-orm-json / 3816986970

pending completion
3816986970

Pull #11

github

GitHub
Merge 5277f4d4a into 6364490a4
Pull Request #11: ci: Fix phinx update issue

898 of 1015 relevant lines covered (88.47%)

43.85 hits per line

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

96.99
/src/Database/Driver/DatFieldSqlDialectTrait.php
1
<?php
2
declare(strict_types=1);
3

4
namespace Lqdt\OrmJson\Database\Driver;
5

6
use Cake\Database\Expression\BetweenExpression;
7
use Cake\Database\Expression\ComparisonExpression;
8
use Cake\Database\Expression\IdentifierExpression;
9
use Cake\Database\Expression\OrderByExpression;
10
use Cake\Database\Expression\OrderClauseExpression;
11
use Cake\Database\Expression\QueryExpression;
12
use Cake\Database\Expression\UnaryExpression;
13
use Cake\Database\ExpressionInterface;
14
use Cake\Database\Query;
15
use Cake\Log\Log;
16
use Cake\ORM\Query as ORMQuery;
17
use Lqdt\OrmJson\Database\Expression\DatFieldExpression;
18
use Lqdt\OrmJson\Database\JsonTypeMap;
19
use Lqdt\OrmJson\DatField\DatFieldParserTrait;
20
use Lqdt\OrmJson\DatField\Exception\MissingPathInDataDatFieldException;
21

22
trait DatFieldSqlDialectTrait
23
{
24
    use DatFieldParserTrait {
25
        DatFieldParserTrait::isDatField as protected _isDatFieldString;
26
    }
27

28
    /** @inheritDoc */
29
    public function isDatField($datfield): int
30
    {
31
        if (is_string($datfield)) {
146✔
32
            return $this->_isDatFieldString($datfield);
146✔
33
        }
34

35
        if ($datfield instanceof DatFieldExpression) {
7✔
36
            return 3;
6✔
37
        }
38

39
        return 0;
2✔
40
    }
41

42
    /**
43
     * @inheritDoc
44
     */
45
    public function quoteIdentifier($identifier): string
46
    {
47
        $identifier = trim($identifier);
10✔
48

49
        // Handles -> and ->> operators available in Mysql and PostgreSql
50
        if (preg_match('/^([\.\w-]+)(\s?)->(.*)$/', $identifier, $matches)) {
10✔
51
            return $this->quoteIdentifier($matches[1]) . $matches[2] . '->' . $matches[3];
5✔
52
        }
53

54
        // Overrides function detection as it does not supports multiple comma separated arguments
55
        // It must be handled before comma detector
56
        if (preg_match('/^([\w-]+)\((.*)\)(\s*AS\s*([\w-]+))?$/', $identifier, $matches)) {
10✔
57
            return empty($matches[4]) ?
5✔
58
              $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ')' :
4✔
59
              $matches[1] . '(' . $this->quoteIdentifier($matches[2]) . ') AS ' . $this->quoteIdentifier($matches[4]);
5✔
60
        }
61

62
        // Handles comma separated multiple arguments, usually in functions
63
        if (strpos($identifier, ',') !== false) {
10✔
64
            $arguments = array_map(function ($arg) {
4✔
65
                return $this->quoteIdentifier($arg);
4✔
66
            }, explode(',', $identifier));
4✔
67

68
            return implode(', ', $arguments);
4✔
69
        }
70

71
        return parent::QuoteIdentifier($identifier);
10✔
72
    }
73

74
    /**
75
     * @inheritDoc
76
     */
77
    public function queryTranslator($type): \Closure
78
    {
79
        // We must preprocess translation in order to let other translations be done with translated datfield
80
        return function (Query $query) use ($type) {
145✔
81
            // Checks that datfield are enabled in this query
82
            $datFieldsEnabled = $this->_getQueryOptions($query, 'useDatFields', true);
145✔
83

84
            try {
85
                switch ($type) {
86
                    case 'select':
145✔
87
                        $query = $this->_selectDatFieldQueryTranslator($query, $datFieldsEnabled);
139✔
88
                        break;
139✔
89
                    case 'insert':
143✔
90
                        $query = $this->_insertDatFieldQueryTranslator($query, $datFieldsEnabled);
143✔
91
                        break;
143✔
92
                    case 'update':
8✔
93
                        $query = $this->_updateDatFieldQueryTranslator($query, $datFieldsEnabled);
3✔
94
                        break;
3✔
95
                    case 'delete':
5✔
96
                        $query = $this->_deleteDatFieldQueryTranslator($query, $datFieldsEnabled);
5✔
97
                        break;
145✔
98
                }
99
            } catch (\Error $err) {
×
100
                // debug($err);
101
                Log::Error(sprintf('Error while processing datfields in query: %s', $err->getMessage()));
×
102
            }
103

104
            // Apply original driver translator transformations
105
            $parentTranslator = parent::QueryTranslator($type);
145✔
106
            $query = $parentTranslator($query);
145✔
107

108
            return $query;
145✔
109
        };
145✔
110
    }
111

112
    /**
113
     * @inheritDoc
114
     */
115
    public function translateExpression($expression, Query $query, JsonTypeMap $jsonTypes)
116
    {
117
        if (is_array($expression)) {
135✔
118
            return array_map(function ($expr) use ($query, $jsonTypes) {
×
119
                return $this->translateExpression($expr, $query, $jsonTypes);
×
120
            }, $expression);
×
121
        }
122

123
        if (is_string($expression)) {
135✔
124
            return $this->_translateRawSQL($expression, $query, $jsonTypes);
20✔
125
        }
126

127
        if ($expression instanceof ComparisonExpression) {
135✔
128
            return $this->_translateComparisonExpression($expression, $query, $jsonTypes);
99✔
129
        }
130

131
        if ($expression instanceof OrderByExpression) {
135✔
132
            return $this->_translateOrderByExpression($expression, $query, $jsonTypes);
23✔
133
        }
134

135
        if ($expression instanceof IdentifierExpression) {
116✔
136
            return $this->_translateIdentifierExpression($expression, $query, $jsonTypes);
19✔
137
        }
138

139
        if ($expression instanceof UnaryExpression) {
116✔
140
            return $this->_translateUnaryExpression($expression, $query, $jsonTypes);
12✔
141
        }
142

143
        if ($expression instanceof BetweenExpression) {
116✔
144
            return $this->_translateBetweenExpression($expression, $query, $jsonTypes);
6✔
145
        }
146

147
        // This one is central as it solely allows nested expressions to be correctly replaced
148
        if ($expression instanceof QueryExpression) {
116✔
149
            $expression->iterateParts(
116✔
150
                function ($expr) use ($query, $jsonTypes) {
116✔
151
                    return $this->translateExpression($expr, $query, $jsonTypes);
116✔
152
                }
116✔
153
            );
116✔
154

155
            return $expression;
116✔
156
        }
157

158
        // For others ExpressionInterface instance, simply sneak in to update content
159
        if ($expression instanceof ExpressionInterface) {
1✔
160
            $expression->traverse(
1✔
161
                function ($expr) use ($query, $jsonTypes) {
1✔
162
                    return $this->translateExpression($expr, $query, $jsonTypes);
1✔
163
                }
1✔
164
            );
1✔
165
        }
166

167
        return $expression;
1✔
168
    }
169

170
    /**
171
     * Returns the model used by query if it is instance of Cake\ORM\Query
172
     * Otherwise returns null
173
     *
174
     * @param \Cake\Database\Query $query Query
175
     * @return string|null
176
     */
177
    protected function _getAliasFromQuery(Query $query): ?string
178
    {
179
        if ($query instanceof ORMQuery) {
2✔
180
            return $query->getRepository()->getAlias();
2✔
181
        }
182

183
        return null;
×
184
    }
185

186
    /**
187
     * Reads available options in query
188
     *
189
     * An option key can be provided to target one specific option. If option is not available, method will return the provided fallback
190
     *
191
     * @param \Cake\Database\Query $query Query
192
     * @param  string|null $option                 Option name
193
     * @param  mixed  $fallback               Fallback value
194
     * @return mixed
195
     */
196
    protected function _getQueryOptions(Query $query, ?string $option = null, $fallback = null)
197
    {
198
        $options = $query instanceof ORMQuery ? $options = $query->getOptions() : [];
145✔
199

200
        return $option ? $options[$option] ?? $fallback : $options;
145✔
201
    }
202

203
    /**
204
     * Processes casters queue and applies it to a whole row
205
     * For SELECT statements, the selectmap must be provided in order to
206
     * parse correctly selected fields from JSON field
207
     *
208
     * @param  array $row                   Row data
209
     * @param  array $casters               Casters queue
210
     * @param \Cake\Database\Query $query   Query
211
     * @param \Lqdt\OrmJson\Database\JsonTypeMap|null $selectmap JSON type map for selected fields. Do not use if not in SELECT statement
212
     * @return array
213
     */
214
    protected function _castRow(array $row, array $casters, Query $query, ?JsonTypeMap $selectmap = null): array
215
    {
216
        $decoder = function ($value) {
143✔
217
            return json_decode($value, true);
3✔
218
        };
143✔
219

220
        foreach ($casters as $datfield => $caster) {
143✔
221
            try {
222
                if ($selectmap) {
17✔
223
                    // Check select type map for automapped aliased field that's need to be decoded before sent to type callback
224
                    $type = $selectmap->type($datfield);
16✔
225
                    if ($type && $type !== '__auto_json__' && !$this->isDatField($datfield)) {
16✔
226
                        /** @var array $row */
227
                        $row = $this->applyCallbackToData($datfield, $row, $decoder);
3✔
228
                    }
229
                }
230

231
                /** @var array $row */
232
                $row = $this->applyCallbackToData($datfield, $row, $caster, $row, $query);
17✔
233
            } catch (MissingPathInDataDatFieldException $err) {
11✔
234
                // Simply ignore error as we don't want to raise an error in case of missing key
235
            }
236
        }
237

238
        return $row;
143✔
239
    }
240

241
    /**
242
     * Translates a given value to be usable for JSON comparisons by casting it to JSON if it's not a string
243
     *
244
     * @param  mixed $value                   Value to translate
245
     * @param \Cake\Database\Query $query     Query
246
     * @param  callable|null $caster          Caster to apply on value before casting it based on core type
247
     * @return mixed
248
     */
249
    protected function _castValue($value, Query $query, ?callable $caster = null)
250
    {
251
        // Apply JSON type caster to value if available
252
        if (is_callable($caster)) {
96✔
253
            $value = $caster($value);
2✔
254
        }
255

256
        // Null case
257
        if (is_null($value)) {
96✔
258
            $value = $query->newExpr("CAST('null' AS JSON)");
2✔
259
        }
260

261
        // Boolean case, simply update value to its stringified JSON counterpart
262
        if (is_bool($value)) {
96✔
263
            $value = $value ? 'true' : 'false';
12✔
264
            $value = $query->newExpr("CAST({$value} AS JSON)");
12✔
265
        }
266

267
        // Number case, we must cast value to JSON to avoid unexpected results with numeric strings
268
        if (is_integer($value) || is_float($value)) {
96✔
269
            $value = $query->newExpr("CAST({$value} AS JSON)");
34✔
270
        }
271

272
        // String or array/object case
273
        if (is_string($value) || is_array($value)) {
96✔
274
            $value = json_encode($value);
51✔
275
            $value = $query->newExpr("CAST('{$value}' AS JSON)");
51✔
276
        }
277

278
        return $value;
96✔
279
    }
280

281
    /**
282
     * Update clauses for select queries
283
     *
284
     * @param \Cake\Database\Query $query Query
285
     * @param  bool $datFieldsEnabled  If `true` datfield notation is enabled and may be used
286
     * @return \Cake\Database\Query Updated query
287
     */
288
    protected function _selectDatFieldQueryTranslator(Query $query, bool $datFieldsEnabled): Query
289
    {
290
        // Update query type map
291
        $map = new JsonTypeMap($query->getTypeMap());
139✔
292
        $selectmap = new JsonTypeMap($query->getSelectTypeMap());
139✔
293

294
        // Translate select clause
295
        if ($datFieldsEnabled) {
139✔
296
            $query->traverse(function ($part, $clause) use ($query, $map, &$selectmap) {
139✔
297
                if ($clause === 'select' && !empty($part)) {
139✔
298
                    $query = $this->_translateSelect($query, $selectmap);
139✔
299
                }
300

301
                // Translate group clause
302
                if ($clause === 'group' && !empty($part)) {
139✔
303
                    $query = $query->group(array_map(function ($field) {
3✔
304
                        /** @phpstan-ignore-next-line */
305
                        return (string)$this->translateDatField($field);
3✔
306
                    }, $part), true);
3✔
307
                }
308

309
                // Translate associations
310
                if ($clause === 'join' && !empty($part)) {
139✔
311
                    foreach ($part as $name => $joint) {
18✔
312
                        $joint['conditions']->traverse(function ($e) use ($query, $map) {
18✔
313
                              $this->translateExpression($e, $query, $map);
18✔
314
                        });
18✔
315
                    }
316
                }
317

318
                if ($part instanceof ExpressionInterface) {
139✔
319
                    $this->translateExpression($part, $query, $map);
121✔
320
                }
321
            });
139✔
322
        }
323

324
        $query->setTypeMap($map->getRegularTypeMap());
139✔
325
        $query->setSelectTypeMap($selectmap->getRegularTypeMap());
139✔
326

327
        // Registers JSON types to be applied to incoming data
328
        $casters = $selectmap->getCasters($query, 'toPHP') + $map->getCasters($query, 'toPHP');
139✔
329
        if (!empty($casters)) {
139✔
330
            $query->decorateResults(function ($row) use ($casters, $query, $selectmap) {
18✔
331
                $row = $this->_castRow($row, $casters, $query, $selectmap);
16✔
332

333
                return $row;
16✔
334
            });
18✔
335
        }
336

337
        return $query;
139✔
338
    }
339

340
    /**
341
     * Update clauses for insert queries to apply JSON types
342
     *
343
     * @param \Cake\Database\Query $query Query
344
     * @param  bool $datFieldsEnabled  If `true` datfield notation is enabled and may be used
345
     * @return \Cake\Database\Query Updated query
346
     */
347
    protected function _insertDatFieldQueryTranslator(Query $query, bool $datFieldsEnabled): Query
348
    {
349
        // Update query type map
350
        $map = new JsonTypeMap($query->getTypeMap());
143✔
351
        $query->setTypeMap($map->getRegularTypeMap());
143✔
352
        $casters = $map->getCasters($query, 'toDatabase');
143✔
353

354
        // We need to apply JSON type map to outgoing data
355
        $expression = $query->clause('values');
143✔
356
        $rows = $expression->getValues();
143✔
357
        foreach ($rows as &$row) {
143✔
358
            $row = $this->_castRow($row, $casters, $query);
143✔
359
        }
360
        $expression->setValues($rows);
143✔
361

362
        return $query;
143✔
363
    }
364

365
    /**
366
     * Update clauses for update queries to apply JSON types and parse where condition
367
     *
368
     * @param \Cake\Database\Query $query Query
369
     * @param  bool $datFieldsEnabled  If `true` datfield notation is enabled and may be used
370
     * @return \Cake\Database\Query Updated query
371
     */
372
    protected function _updateDatFieldQueryTranslator(Query $query, bool $datFieldsEnabled): Query
373
    {
374
        // Update query type map
375
        $map = new JsonTypeMap($query->getTypeMap());
3✔
376
        $query->setTypeMap($map->getRegularTypeMap());
3✔
377

378
        // We need to apply JSON type map to outgoing data
379
        $set = $query->clause('set');
3✔
380
        $set->iterateParts(function ($expr) use ($query, $map) {
3✔
381
            if ($expr instanceof ComparisonExpression) {
3✔
382
                return $this->translateSetDatField($expr, $query, $map);
3✔
383
            }
384

385
            return $expr;
×
386
        });
3✔
387

388
        // No where parsing if datfields are disabled
389
        if ($datFieldsEnabled) {
3✔
390
            $where = $query->clause('where');
3✔
391
            $this->translateExpression($where, $query, $map);
3✔
392
        }
393

394
        return $query;
3✔
395
    }
396

397
    /**
398
     * Update clauses for delete queries to parse where conditions
399
     *
400
     * @param \Cake\Database\Query $query Query
401
     * @param  bool $datFieldsEnabled  If `true` datfield notation is enabled and may be used
402
     * @return \Cake\Database\Query Updated query
403
     */
404
    protected function _deleteDatFieldQueryTranslator(Query $query, bool $datFieldsEnabled): Query
405
    {
406
        if (!$datFieldsEnabled) {
5✔
407
            return $query;
×
408
        }
409

410
        $where = $query->clause('where');
5✔
411
        $map = new JsonTypeMap($query->getTypeMap());
5✔
412
        $query->setTypeMap($map->getRegularTypeMap());
5✔
413
        $this->translateExpression($where, $query, $map);
5✔
414

415
        return $query;
5✔
416
    }
417

418
    /**
419
     * Translates a between expression
420
     *
421
     * @param \Cake\Database\Expression\BetweenExpression $expression Expression
422
     * @param \Cake\Database\Query $query Query
423
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
424
     * @return \Cake\Database\Expression\BetweenExpression Translated expression
425
     */
426
    protected function _translateBetweenExpression(
427
        BetweenExpression $expression,
428
        Query $query,
429
        JsonTypeMap $jsonTypes
430
    ): BetweenExpression {
431
        $field = $this->translateDatField($expression->getField());
6✔
432
        $expression->setField($field);
6✔
433

434
        if ($this->isDatField($field)) {
6✔
435
            $caster = $jsonTypes->getCaster($field, $query);
6✔
436
            $reflection = new \ReflectionClass($expression);
6✔
437
            $from = $reflection->getProperty('_from');
6✔
438
            $to = $reflection->getProperty('_to');
6✔
439
            $from->setAccessible(true);
6✔
440
            $to->setAccessible(true);
6✔
441
            $tfrom = $this->_castValue($from->getValue($expression), $query, $caster);
6✔
442
            $tto = $this->_castValue($to->getValue($expression), $query, $caster);
6✔
443
            $from->setValue($expression, $tfrom);
6✔
444
            $to->setValue($expression, $tto);
6✔
445
        }
446

447
        return $expression;
6✔
448
    }
449

450
    /**
451
     * Update or replace the ComparisonExpression expression to perform ComparisonExpressions on
452
     * datFields. In some cases, PDO limitations implies to replace the
453
     * expression with a raw SQL fragment. It can be a dangerous when
454
     * using raw user input to perform global matching in `array` mode.
455
     *
456
     * Regular fields expressions are left as is.
457
     *
458
     * @version 1.0.4
459
     * @since   1.5.0
460
     * @param \Cake\Database\Expression\ComparisonExpression $expression ComparisonExpression expression
461
     * @param \Cake\Database\Query $query Query
462
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
463
     * @return \Cake\Database\Expression\ComparisonExpression|\Cake\Database\Expression\QueryExpression Updated expression
464
     */
465
    protected function _translateComparisonExpression(
466
        ComparisonExpression $expression,
467
        Query $query,
468
        JsonTypeMap $jsonTypes
469
    ) {
470
        $field = $expression->getField();
99✔
471

472
        // Checks if it's a datfield and transform value if needed
473
        if ($this->isDatField($field)) {
99✔
474
            $caster = $jsonTypes->getCaster($field, $query);
90✔
475
            // Disable alias for update and delete queries
476
            $repository = in_array($query->type(), ['update', 'delete']) ? false : null;
90✔
477
            $field = $this->translateDatField($field, false, $repository);
90✔
478
            $value = $expression->getValue();
90✔
479
            $operator = strtolower($expression->getOperator());
90✔
480
            if ($operator === 'in' && is_array($value)) {
90✔
481
                foreach ($value as &$item) {
14✔
482
                    $item = $this->_castValue($item, $query);
14✔
483
                }
484
            } else {
485
                $value = $this->_castValue($value, $query, $caster);
81✔
486
            }
487

488
            $expression->setValue($value);
90✔
489
            $expression->setField($field);
90✔
490
        }
491

492
        return $expression;
99✔
493
    }
494

495
    /**
496
     * Translates an IdentifierExpression. Identifier is always unquoted as it can be used
497
     * in complex OrderClauseExpression. Resulting DatFieldExpression is converted to string.
498
     *
499
     * @param \Cake\Database\Expression\IdentifierExpression $expression Expression
500
     * @param \Cake\Database\Query $query If `true returns indentifier instead of updated expression
501
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
502
     * @return string|\Cake\Database\Expression\IdentifierExpression SQL fragment or query expression
503
     */
504
    protected function _translateIdentifierExpression(
505
        IdentifierExpression $expression,
506
        Query $query,
507
        JsonTypeMap $jsonTypes
508
    ) {
509
        $field = $expression->getIdentifier();
19✔
510

511
        if ($this->isDatField($field)) {
19✔
512
            /** @phpstan-ignore-next-line */
513
            $field = (string)$this->translateDatField($field, true);
19✔
514
            $expression->setIdentifier($field);
19✔
515
        }
516

517
        return $expression;
19✔
518
    }
519

520
    /**
521
     * Translates IS NULL statements into a suitable SQL for JSON data
522
     *
523
     * @param  string          $datfield                Datfield
524
     * @param \Cake\Database\Query $query Query
525
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
526
     * @return \Cake\Database\Expression\QueryExpression IS NULL Expression
527
     */
528
    protected function _translateIsNull(
529
        string $datfield,
530
        Query $query,
531
        JsonTypeMap $jsonTypes
532
    ): QueryExpression {
533
        /** @phpstan-ignore-next-line */
534
        $datfield = (string)$this->translateDatField($datfield);
5✔
535
        $ignoreMissingPath = $this->_getQueryOptions($query, 'ignoreMissingPath', false);
5✔
536

537
        return $ignoreMissingPath ?
5✔
538
          $query->newExpr("{$datfield} = CAST('null' AS JSON)") :
1✔
539
          $query->newExpr()->or([
5✔
540
            "{$datfield} IS" => null,
5✔
541
            $query->newExpr("{$datfield} = CAST('null' AS JSON)"),
5✔
542
        ]);
5✔
543
    }
544

545
    /**
546
     * Translates IS NOT NULL statements into a suitable SQL for JSON data
547
     *
548
     * @param  string          $datfield                Datfield
549
     * @param \Cake\Database\Query $query Query
550
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
551
     * @return \Cake\Database\Expression\QueryExpression IS NULL Expression
552
     */
553
    protected function _translateIsNotNull(string $datfield, Query $query, JsonTypeMap $jsonTypes): QueryExpression
554
    {
555
        /** @phpstan-ignore-next-line */
556
        $datfield = (string)$this->translateDatField($datfield);
4✔
557

558
        return $query->newExpr("{$datfield} <> CAST('null' AS JSON)");
4✔
559
    }
560

561
    /**
562
     * Updates fields in order clause by JSON_EXTRACT equivalent
563
     *
564
     * @param \Cake\Database\Expression\OrderByExpression $expression Order expression
565
     * @param \Cake\Database\Query $query Query
566
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
567
     * @return \Cake\Database\Expression\OrderByExpression Updated Order expression
568
     */
569
    protected function _translateOrderByExpression(
570
        OrderByExpression $expression,
571
        Query $query,
572
        JsonTypeMap $jsonTypes
573
    ): OrderByExpression {
574
        $expression->iterateParts(function ($fieldOrOrder, &$key) use ($query, $jsonTypes) {
23✔
575
            if ($fieldOrOrder instanceof OrderClauseExpression) {
23✔
576
                return $this->translateExpression($fieldOrOrder, $query, $jsonTypes);
1✔
577
            }
578

579
            if ($this->isDatField($fieldOrOrder)) {
22✔
580
                /** @phpstan-ignore-next-line */
581
                return (string)$this->translateDatField($fieldOrOrder);
12✔
582
            }
583

584
            if ($this->isDatField($key)) {
11✔
585
                /** @phpstan-ignore-next-line */
586
                $key = (string)$this->translateDatField($key);
9✔
587
            }
588

589
            return $fieldOrOrder;
11✔
590
        });
23✔
591

592
        return $expression;
23✔
593
    }
594

595
    /**
596
     * Parsed DatField as string or in SQL fragments
597
     *
598
     * @param  string   $fragment   SQL fragment to parse
599
     * @param \Cake\Database\Query $query Query
600
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
601
     * @return \Lqdt\OrmJson\Database\Expression\DatFieldExpression|string Updated fragment
602
     */
603
    protected function _translateRawSQL(string $fragment, Query $query, JsonTypeMap $jsonTypes)
604
    {
605
        if ($this->isDatField($fragment)) {
138✔
606
            /** @var \Lqdt\OrmJson\Database\Expression\DatFieldExpression $expr */
607
            $expr = $this->translateDatField($fragment);
2✔
608

609
            return $expr;
2✔
610
        }
611

612
        // Avoid translating already CAST TO JSON strings as it can mess up
613
        if (preg_match('/^CAST\(.*AS JSON\)$/', $fragment)) {
136✔
614
            return $fragment;
10✔
615
        }
616

617
        return preg_replace_callback(
136✔
618
            '/[\w\.\*\[\]]+(@|->)[\w\.\*\[\]]+/',
136✔
619
            function (array $matches) {
136✔
620
                /** @var \Lqdt\OrmJson\Database\Expression\DatFieldExpression $expr */
621
                $expr = $this->translateDatField($matches[0]);
1✔
622

623
                return (string)$expr;
1✔
624
            },
136✔
625
            $fragment
136✔
626
        );
136✔
627
    }
628

629
    /**
630
     * Apply datfield notation to select queries and handle select typemap for JSON fields
631
     * It will also update provided JSON type map to allow casting of aliases datfields
632
     *
633
     * @param \Cake\Database\Query $query Query
634
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $map JSON Type map
635
     * @return \Cake\Database\Query Updated query
636
     */
637
    protected function _translateSelect(Query $query, JsonTypeMap &$map): Query
638
    {
639
        $fields = $query->clause('select');
139✔
640
        $updatedFields = [];
139✔
641

642
        // Handle alias for selected datfields
643
        foreach ($fields as $alias => $field) {
139✔
644
            $palias = null;
139✔
645

646
            // If field is an expression, translate it
647
            if ($field instanceof ExpressionInterface) {
139✔
648
                $updatedFields[$alias] = $this->translateExpression($field, $query, $map);
13✔
649
                continue;
13✔
650
            }
651

652
            if (is_int($field)) {
136✔
653
                $updatedFields[$alias] = $field;
9✔
654
                continue;
9✔
655
            }
656

657
            // Regular field case
658
            if (!$this->isDatField($field)) {
135✔
659
                $updatedFields[$alias] = $this->_translateRawSQL($field, $query, $map);
132✔
660
                continue;
132✔
661
            }
662

663
            if (is_integer($alias) || $this->isDatField($alias)) {
9✔
664
                // No alias given or autogenerated, generates a valid one or query will fail
665
                // Also clean CakePHP processed alias to keep JSON structure
666
                // This will only work if using \Cake\ORM\Query
667

668
                // If field is selected without alias, we must link type to previous cakephp alias
669
                $palias = is_string($alias) ? $alias : null;
3✔
670
                $alias = $this->aliasDatField($field);
3✔
671

672
                // Restore model alias
673
                if ($query instanceof ORMQuery) {
3✔
674
                    $alias = sprintf('%s__%s', $this->_getAliasFromQuery($query), $alias);
3✔
675
                }
676
            } else {
677
                // When aliasing datfields, select type map will only receive alias with copied type
678
                // We must get back type from main type map and clears select type map
679
                $type = $query->getTypeMap()->toArray()[$field] ?? null;
9✔
680
                if ($type) {
9✔
681
                    // Add back JSON type to parse alias correctly
682
                    $map
2✔
683
                      ->addJsonType($field, $type)
2✔
684
                      ->clearRegularTypeMap($alias);
2✔
685
                }
686
            }
687

688
            // Update alias target
689
            $map->setAlias($palias ?? $field, $alias);
9✔
690
            $updatedFields[$alias] = $this->translateDatField($field);
9✔
691
        }
692

693
        $query->select($updatedFields, true);
139✔
694

695
        return $query;
139✔
696
    }
697

698
    /**
699
     * Converts and unary expression
700
     *
701
     * It's quite hacky as UnaryExpression doesn't expose getters for `value` and `operator`
702
     *
703
     * It is used to parse 'IS NULL' or 'IS NOT NULL' statements
704
     *
705
     * @param \Cake\Database\Expression\UnaryExpression $expression Expression
706
     * @param \Cake\Database\Query $query Query
707
     * @param \Lqdt\OrmJson\Database\JsonTypeMap $jsonTypes JSON type map
708
     * @return \Cake\Database\Expression\UnaryExpression|\Cake\Database\Expression\QueryExpression Updated expression
709
     */
710
    protected function _translateUnaryExpression(UnaryExpression $expression, Query $query, JsonTypeMap $jsonTypes)
711
    {
712
        $reflection = new \ReflectionClass($expression);
12✔
713
        $operator = $reflection->getProperty('_operator');
12✔
714
        $operator->setAccessible(true);
12✔
715
        $op = $operator->getValue($expression);
12✔
716
        $value = $reflection->getProperty('_value');
12✔
717
        $value->setAccessible(true);
12✔
718
        $value = $value->getValue($expression);
12✔
719

720
        switch ($op) {
721
            case 'IS NULL':
12✔
722
                $datfield = $value->getIdentifier();
7✔
723
                if ($this->isDatField($datfield)) {
7✔
724
                    $expression = $this->_translateIsNull($datfield, $query, $jsonTypes);
5✔
725
                }
726
                break;
7✔
727
            case 'IS NOT NULL':
7✔
728
                $datfield = $value->getIdentifier();
4✔
729
                if ($this->isDatField($datfield)) {
4✔
730
                    $expression = $this->_translateIsNotNull($datfield, $query, $jsonTypes);
4✔
731
                }
732
                break;
4✔
733
            default:
734
                if ($value instanceof ExpressionInterface) {
3✔
735
                    $this->translateExpression($value, $query, $jsonTypes);
3✔
736
                }
737
                break;
3✔
738
        }
739

740
        return $expression;
12✔
741
    }
742
}
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