Failed Conditions
Push — refactor/improve-static-analys... ( c6edde...45b897 )
by Bas
07:40
created

Grammar::compileUpsert()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 17
c 1
b 0
f 0
nc 8
nop 4
dl 0
loc 25
rs 9.7
ccs 12
cts 12
cp 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace LaravelFreelancerNL\Aranguent\Query;
6
7
use Illuminate\Database\Query\Builder as IlluminateQueryBuilder;
8
use Illuminate\Database\Query\Expression;
9
use Illuminate\Database\Query\Grammars\Grammar as IlluminateQueryGrammar;
10
use Illuminate\Support\Arr;
11
use Illuminate\Support\Traits\Macroable;
12
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesAggregates;
13
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesColumns;
14
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesFilters;
15
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesGroups;
16
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesInserts;
17
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesJoins;
18
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesUnions;
19
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesWheres;
20
use LaravelFreelancerNL\Aranguent\Query\Concerns\ConvertsIdToKey;
21
use LaravelFreelancerNL\Aranguent\Query\Concerns\HandlesAqlGrammar;
22
23
class Grammar extends IlluminateQueryGrammar
24
{
25
    use CompilesAggregates;
0 ignored issues
show
Bug introduced by
The trait LaravelFreelancerNL\Aran...erns\CompilesAggregates requires the property $from which is not provided by LaravelFreelancerNL\Aranguent\Query\Grammar.
Loading history...
26
    use CompilesColumns;
0 ignored issues
show
introduced by
The trait LaravelFreelancerNL\Aran...oncerns\CompilesColumns requires some properties which are not provided by LaravelFreelancerNL\Aranguent\Query\Grammar: $groups, $returnSingleValue, $distinct, $from, $joins, $table, $tableAliases
Loading history...
27
    use CompilesFilters;
0 ignored issues
show
Bug introduced by
The trait LaravelFreelancerNL\Aran...oncerns\CompilesFilters requires the property $havings which is not provided by LaravelFreelancerNL\Aranguent\Query\Grammar.
Loading history...
28
    use CompilesInserts;
0 ignored issues
show
Bug introduced by
The trait LaravelFreelancerNL\Aran...oncerns\CompilesInserts requires the property $from which is not provided by LaravelFreelancerNL\Aranguent\Query\Grammar.
Loading history...
29
    use CompilesJoins;
0 ignored issues
show
introduced by
The trait LaravelFreelancerNL\Aran...\Concerns\CompilesJoins requires some properties which are not provided by LaravelFreelancerNL\Aranguent\Query\Grammar: $type, $table, $grammar
Loading history...
30
    use CompilesGroups;
31
    use CompilesUnions;
0 ignored issues
show
introduced by
The trait LaravelFreelancerNL\Aran...Concerns\CompilesUnions requires some properties which are not provided by LaravelFreelancerNL\Aranguent\Query\Grammar: $unions, $unionOrders, $unionOffset, $unionLimit
Loading history...
32
    use CompilesWheres;
33
    use ConvertsIdToKey;
34
    use HandlesAqlGrammar;
35
    use Macroable;
36
37
    public $name;
38
39
    /**
40
     * The grammar table prefix.
41
     *
42
     * @var string
43
     */
44
    protected $tablePrefix = '';
45
46
    /**
47
     * The grammar table prefix.
48
     *
49
     * @var null|int
50
     */
51
    protected $offset = null;
52
53
    /**
54
     * The grammar specific operators.
55
     *
56
     * @var array
57
     */
58
    protected $operators = [
59
        '==', '!=', '<', '>', '<=', '>=',
60
        'LIKE', '~', '!~',
61
        'IN', 'NOT IN',
62
        'ALL ==', 'ALL !=', 'ALL <', 'ALL >', 'ALL <=', 'ALL >=', 'ALL IN',
63
        'ANY ==', 'ANY !=', 'ANY <', 'ANY >', 'ANY <=', 'ANY >=', 'ANY IN',
64
        'NONE ==', 'NONE !=', 'NONE <', 'NONE >', 'NONE <=', 'NONE >=', 'NONE IN',
65
    ];
66
67
    /**
68
     * The components that make up a select clause.
69
     *
70
     * @var array<string>
71
     */
72
    protected $selectComponents = [
73
        'preIterationVariables',
74
        'from',
75
        'search',
76
        'joins',
77
        'postIterationVariables',
78
        'wheres',
79
        'groups',
80
        'aggregate',
81
        'havings',
82
        'orders',
83
        'offset',
84
        'limit',
85
        'columns',
86 49
    ];
87
88 49
    protected $operatorTranslations = [
89
        '='          => '==',
90
        '<>'         => '!=',
91
        '<=>'        => '==',
92
        'rlike'      => '=~',
93
        'not rlike'  => '!~',
94
        'regexp'     => '=~',
95
        'not regexp' => '!~',
96 12
    ];
97
98 12
    protected $whereTypeOperators = [
99
        'In'    => 'IN',
100
        'NotIn' => 'NOT IN',
101 162
    ];
102
103 162
    /**
104
     * The grammar specific bitwise operators.
105
     *
106
     * @var array
107
     */
108
    public $bitwiseOperators = [
109
        '&', '|', '^', '<<', '>>', '~',
110
    ];
111
112
    /**
113
     * Get the format for database stored dates.
114
     *
115
     * @return string
116 44
     */
117
    public function getDateFormat()
118 44
    {
119 1
        return 'Y-m-d\TH:i:s.vp';
120
    }
121 44
122
    /**
123 44
     * Get the grammar specific operators.
124
     *
125
     * @return array
126
     */
127
    public function getOperators()
128
    {
129
        return $this->operators;
130 44
    }
131 44
132
133
    public function translateOperator(string $operator): string
134 44
    {
135 44
        if (array_key_exists($operator, $this->operatorTranslations)) {
136 44
            return $this->operatorTranslations[$operator];
137 44
        }
138
139 44
        return $operator;
140
    }
141
142
    protected function prefixTable($table)
143
    {
144
        return $this->tablePrefix . $table;
145
    }
146
147 10
148
    /**
149 10
     * Get the appropriate query parameter place-holder for a value.
150 10
     *
151
     * @param  mixed  $value
152 10
     * @return string
153
     */
154 10
    public function parameter($value)
155 9
    {
156
157
        return $this->isExpression($value) ? $this->getValue($value) : $value;
158 10
    }
159
160
    /**
161
     * Compile the components necessary for a select clause.
162
     *
163
     * @param  IlluminateQueryBuilder  $query
164
     * @return array
165
     */
166 10
    protected function compileComponents(IlluminateQueryBuilder $query)
167 10
    {
168
        $aql = [];
169
170 10
        foreach ($this->selectComponents as $component) {
171 10
            if ($component === 'unions') {
172 10
                continue;
173 10
            }
174
175 10
            if (isset($query->$component)) {
176
                $method = 'compile' . ucfirst($component);
177
178
                $aql[$component] = $this->$method($query, $query->$component);
179
            }
180
        }
181
182
        return $aql;
183
    }
184
185 98
186
187 98
    /**
188 98
     * Compile a select query into SQL.
189
     *
190 98
     * @param IlluminateQueryBuilder $query
191
     * @return string
192 98
     */
193
    public function compileSelect(IlluminateQueryBuilder $query)
194
    {
195
        assert($query instanceof Builder);
196
197
        // If the query does not have any columns set, we'll set the columns to the
198
        // * character to just get all of the columns from the database. Then we
199 98
        // can build the query and concatenate all the pieces together as one.
200 98
        $original = $query->columns;
201
202
        if (empty($query->columns)) {
203 98
            $query->columns = ['*'];
204 98
        }
205 98
206 98
        // To compile the query, we'll spin through each component of the query and
207 98
        // see if that component exists. If it does we'll just call the compiler
208
        // function for the component which is responsible for making the SQL.
209 98
210
        $aql = trim(
211
            $this->concatenate(
212
                $this->compileComponents($query)
213
            )
214
        );
215
216
        //        if ($query->unions && $query->aggregate) {
217
        //            return $this->compileUnionAggregate($query);
218
        //        }
219
        if ($query->unions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query->unions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
220 146
            return $this->compileUnions($query, $aql);
221
        }
222
223
        $query->columns = $original;
224
225
        if ($query->groupVariables !== null) {
226
            $query->cleanGroupVariables();
227
        }
228
229
        return $aql;
230 146
    }
231
232
    /**
233
     * Compile a truncate table statement into SQL.
234
     *
235
     * @param  IlluminateQueryBuilder  $query
236 146
     * @return array
237
     */
238
    public function compileTruncate(IlluminateQueryBuilder $query)
239
    {
240
        return [$this->compileDelete($query) => []];
241
    }
242
243
    /**
244
     * Compile the "from" portion of the query -> FOR in AQL.
245 1
     *
246
     * @param IlluminateQueryBuilder $query
247
     * @param string  $table
248 1
     *
249 1
     * @return string
250 1
     */
251
    protected function compileFrom(IlluminateQueryBuilder $query, $table)
252
    {
253
        assert($query instanceof Builder);
254
255
        // FIXME: wrapping/quoting
256
        $table = $this->prefixTable($table);
257
258
        //FIXME: register given alias (x AS y in SQL)
259
        $alias = $query->registerTableAlias($table);
260 146
261
262 146
        return "FOR $alias IN $table";
263
    }
264
265
    /**
266
     * @param IlluminateQueryBuilder $query
267 146
     * @param array $variables
268 146
     * @return string
269
     */
270 146
    protected function compilePreIterationVariables(IlluminateQueryBuilder $query, array $variables): string
271
    {
272
        return $this->compileVariables($query, $variables);
273
    }
274 146
275
    /**
276
     * @param IlluminateQueryBuilder $query
277
     * @param array $variables
278
     * @return string
279
     */
280
    protected function compilePostIterationVariables(IlluminateQueryBuilder $query, array $variables): string
281
    {
282
        return $this->compileVariables($query, $variables);
283
    }
284
285 146
286
    /**
287 146
     * @param IlluminateQueryBuilder $query
288 146
     * @param array $variables
289
     * @return string
290 146
     */
291
    protected function compileVariables(IlluminateQueryBuilder $query, array $variables): string
292 146
    {
293
        $aql = '';
294
295
        foreach ($variables as $variable => $value) {
296
            if ($value instanceof Expression) {
297
                $value = $value->getValue($this);
298
            }
299
300 146
            $aql .= ' LET ' . $variable . ' = ' . $value;
301
        }
302 146
303 1
        return trim($aql);
304 1
    }
305
306
    /**
307
     * Compile the "order by" portions of the query.
308 146
     *
309
     * @param  \Illuminate\Database\Query\Builder  $query
310
     * @param  array  $orders
311
     * @return string
312
     */
313
    protected function compileOrders(IlluminateQueryBuilder $query, $orders, $table = null)
314
    {
315
        if (!empty($orders)) {
316
            return 'SORT ' . implode(', ', $this->compileOrdersToArray($query, $orders, $table));
317
        }
318
319 3
        return '';
320
    }
321 3
322 3
    /**
323 3
     * Compile the query orders to an array.
324
     *
325 3
     * @param  \Illuminate\Database\Query\Builder  $query
326
     * @param  array  $orders
327
     * @return array
328
     */
329
    protected function compileOrdersToArray(IlluminateQueryBuilder $query, $orders, $table = null)
330
    {
331
        return array_map(function ($order) use ($query, $table) {
332
            $key = 'column';
333
            if (array_key_exists('sql', $order)) {
334
                $key = 'sql';
335
            }
336
337
            if ($order[$key] instanceof Expression) {
338
                $order[$key] = $order[$key]->getValue($this);
339 3
            } else {
340
                $order[$key] = $this->normalizeColumn($query, $order[$key], $table);
341 3
            }
342
343 3
            return array_key_exists('direction', $order) ? $order[$key] . ' ' . $order['direction'] : $order[$key];
344 3
        }, $orders);
345 2
    }
346
347
    /**
348 3
     * Compile the "offset" portions of the query.
349
     *
350 3
     * @param  \Illuminate\Database\Query\Builder  $query
351 2
     * @param  int  $offset
352
     * @return string
353
     */
354
    protected function compileOffset(IlluminateQueryBuilder $query, $offset)
355 3
    {
356
        $this->offset = (int) $offset;
357
358
        return "";
359
    }
360
361
    /**
362
     * Compile the "limit" portions of the query.
363
     *
364
     * @param  \Illuminate\Database\Query\Builder  $query
365
     * @param  int  $limit
366
     * @return string
367 4
     */
368
    protected function compileLimit(IlluminateQueryBuilder $query, $limit)
369 4
    {
370
        if ($this->offset !== null) {
371 4
            return "LIMIT " . (int) $this->offset . ", " . (int) $limit;
372
        }
373
374
        return "LIMIT " . (int) $limit;
375
    }
376
377
    protected function createUpdateObject($values)
378
    {
379
        $valueStrings = [];
380
        foreach($values as $key => $value) {
381
            if (is_array($value)) {
382 72
                $valueStrings[] = $key . ': ' . $this->createUpdateObject($value);
383
            } else {
384 72
                $valueStrings[] = $key . ': ' . $value;
385 4
            }
386
        }
387 4
388
        return '{ ' . implode(', ', $valueStrings) . ' }';
389 68
    }
390
391 68
    /**
392
     * Compile an update statement into AQL.
393
     *
394
     * @param  \Illuminate\Database\Query\Builder  $query
395
     * @param  array  $values
396
     * @return string
397
     */
398
    public function compileUpdate(IlluminateQueryBuilder $query, array|string $values)
399
    {
400
        assert($query instanceof Builder);
401
402
        $table = $query->from;
403 23
        $alias = $query->getTableAlias($query->from);
404
405
        if (!is_array($values)) {
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
406 23
            $values = Arr::wrap($values);
407 23
        }
408
409 23
        $updateValues = $this->generateAqlObject($values);
410
411
        $aqlElements = [];
412 23
        $aqlElements[] = $this->compileFrom($query, $query->from);
413
414 23
        if (!empty($query->joins)) {
415
            $aqlElements[] = $this->compileJoins($query, $query->joins);
416 23
        }
417
418
        $aqlElements[] = $this->compileWheres($query);
419
420
        $aqlElements[] = 'UPDATE ' . $alias . ' WITH ' . $updateValues . ' IN ' . $table;
421
422
        return implode(' ', $aqlElements);
423
    }
424
425
    /**
426
     * Compile an "upsert" statement into AQL.
427
     *
428
     * @param  \Illuminate\Database\Query\Builder  $query
429
     * @param  array  $values
430 1
     * @param  array  $uniqueBy
431
     * @param  array  $update
432
     * @return string
433 1
     */
434 1
    public function compileUpsert(IlluminateQueryBuilder $query, array $values, array $uniqueBy, array $update)
435
    {
436
        $searchFields = [];
437 1
        foreach($uniqueBy as $key => $field) {
438 1
            $searchFields[$field] = 'doc.' . $field;
439
        }
440
        $searchObject = $this->generateAqlObject($searchFields);
441 1
442 1
        $updateFields = [];
443
        foreach($update as $key => $field) {
444
            $updateFields[$field] = 'doc.' . $field;
445
        }
446 1
        $updateObject = $this->generateAqlObject($updateFields);
447 1
448 1
        $valueObjects = [];
449 1
        foreach($values as $data) {
450 1
            $valueObjects[] = $this->generateAqlObject($data);
451 1
        }
452
453 1
        return 'LET docs = [' . implode(', ', $valueObjects) . ']'
454
            . ' FOR doc IN docs'
455
            . ' UPSERT ' . $searchObject
456
            . ' INSERT doc'
457
            . ' UPDATE ' . $updateObject
458
            . ' IN ' . $query->from;
459
    }
460
461
    /**
462
     * Compile a delete statement into SQL.
463
     *
464
     * @param  \Illuminate\Database\Query\Builder  $query
465
     * @return string
466
     */
467 12
    public function compileDelete(IlluminateQueryBuilder $query)
468
    {
469 12
        $table = $query->from;
470 12
471
        $where = $this->compileWheres($query);
472
473 12
        return trim(
474 1
            !empty($query->joins)
475
                ? $this->compileDeleteWithJoins($query, $table, $where)
476 1
                : $this->compileDeleteWithoutJoins($query, $table, $where)
477
        );
478
    }
479 12
480
481
    /**
482 12
     * Compile a delete statement without joins into SQL.
483
     *
484 12
     * @param  \Illuminate\Database\Query\Builder  $query
485
     * @param  string  $table
486 12
     * @param  string  $where
487
     * @return string
488
     */
489
    protected function compileDeleteWithoutJoins(IlluminateQueryBuilder $query, $table, $where)
490
    {
491
        assert($query instanceof Builder);
492
493
        $alias = $this->normalizeColumn($query, $query->registerTableAlias($table));
494
495
        $table = $this->wrapTable($this->prefixTable($table));
496 1
497
        return "FOR {$alias} IN {$table} {$where} REMOVE {$alias} IN {$table}";
498 1
    }
499
500
    /**
501
     * Compile the random statement into SQL.
502
     *
503
     * @param  string|int|null  $seed
504
     * @return string
505 7
     */
506
    public function compileRandom($seed = null)
507 7
    {
508
        unset($seed);
509 7
510 3
        return "RAND()";
511
    }
512
513 7
    /**
514
     * @param IlluminateQueryBuilder $query
515
     * @return string
516
     * @throws \Exception
517
     */
518
    public function compileSearch(IlluminateQueryBuilder $query, array $search)
519
    {
520
        $predicates = [];
521
        foreach($search['fields'] as $field) {
522 1
            $predicates[] = $this->normalizeColumn($query, $field)
523
                . ' IN TOKENS(' . $search['searchText'] . ', "text_en")';
524 1
        }
525
526
        return 'SEARCH ANALYZER('
527
            . implode(' OR ', $predicates)
528
            . ', "text_en")';
529
    }
530
531
    /**
532
     * Get the value of a raw expression.
533
     *
534
     * @param Expression $expression
535
     * @return string
536
     */
537
    public function getValue($expression)
538
    {
539
        if (!$expression instanceof Expression) {
0 ignored issues
show
introduced by
$expression is always a sub-type of Illuminate\Database\Query\Expression.
Loading history...
540
            return $expression;
541
        }
542
543
        return $expression->getValue($this);
544
    }
545
546
    /**
547
     * Get the grammar specific bit operators.
548
     *
549
     * @return array
550
     */
551
    public function getBitwiseOperators()
552
    {
553
        return $this->bitwiseOperators;
554
    }
555
556
    /**
557
     * Prepare the bindings for a delete statement.
558
     *
559
     * @param  array  $bindings
560
     * @return array
561
     */
562
    public function prepareBindingsForDelete(array $bindings)
563
    {
564
        return Arr::collapse(
565
            Arr::except($bindings, 'select')
566
        );
567
    }
568
569
    /**
570
     * Determine if the given string is a JSON selector.
571
     *
572
     * @param  string  $value
573
     * @return bool
574
     */
575
    public function isJsonSelector($value)
576
    {
577
        if(!is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
578
            return false;
579
        }
580
581
        return str_contains($value, '->');
582
    }
583
584
    public function convertJsonFields($data): mixed
585
    {
586
        if (!is_array($data) && !is_string($data)) {
587
            return $data;
588
        }
589
590
        if (is_string($data)) {
591
            return str_replace('->', '.', $data);
592
        }
593
594
        if (array_is_list($data)) {
595
            return $this->convertJsonValuesToDotNotation($data);
596
        }
597
598
        return $this->convertJsonKeysToDotNotation($data);
599
    }
600
601
    public function convertJsonValuesToDotNotation(array $fields): array
602
    {
603
        foreach($fields as $key => $value) {
604
            if ($this->isJsonSelector($value)) {
605
                $fields[$key] = str_replace('->', '.', $value);
606
            }
607
        }
608
        return $fields;
609
    }
610
611
    public function convertJsonKeysToDotNotation(array $fields): array
612
    {
613
        foreach($fields as $key => $value) {
614
            if ($this->isJsonSelector($key)) {
615
                $fields[str_replace('->', '.', $key)] = $value;
616
                unset($fields[$key]);
617
            }
618
        }
619
        return $fields;
620
    }
621
}
622