Failed Conditions
Push — refactor/improve-static-analys... ( 740b03...f2a08f )
by Bas
06:58 queued 12s
created

Grammar::compileOrders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 0
cts 0
cp 0
rs 10
cc 2
nc 2
nop 2
crap 6
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\CompilesJoins;
17
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesUnions;
18
use LaravelFreelancerNL\Aranguent\Query\Concerns\CompilesWheres;
19
use LaravelFreelancerNL\Aranguent\Query\Concerns\ConvertsIdToKey;
20
use LaravelFreelancerNL\Aranguent\Query\Concerns\HandlesAqlGrammar;
21
use LaravelFreelancerNL\FluentAQL\Exceptions\BindException as BindException;
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: $variables, $groups, $distinct, $from, $joins, $table
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 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...
29
    use CompilesGroups;
30
    use CompilesUnions;
0 ignored issues
show
Bug introduced by
The trait LaravelFreelancerNL\Aran...Concerns\CompilesUnions requires the property $unions which is not provided by LaravelFreelancerNL\Aranguent\Query\Grammar.
Loading history...
31
    use CompilesWheres;
32
    use ConvertsIdToKey;
33
    use HandlesAqlGrammar;
34
    use Macroable;
35
36
    public $name;
37
38
    /**
39
     * The grammar table prefix.
40
     *
41
     * @var string
42
     */
43
    protected $tablePrefix = '';
44
45
    /**
46
     * The grammar table prefix.
47
     *
48
     * @var null|int
49
     */
50
    protected $offset = null;
51
52
    /**
53
     * The grammar specific operators.
54
     *
55
     * @var array
56
     */
57
    protected $operators = [
58
        '==', '!=', '<', '>', '<=', '>=',
59
        'LIKE', '~', '!~',
60
        'IN', 'NOT IN',
61
        'ALL ==', 'ALL !=', 'ALL <', 'ALL >', 'ALL <=', 'ALL >=', 'ALL IN',
62
        'ANY ==', 'ANY !=', 'ANY <', 'ANY >', 'ANY <=', 'ANY >=', 'ANY IN',
63
        'NONE ==', 'NONE !=', 'NONE <', 'NONE >', 'NONE <=', 'NONE >=', 'NONE IN',
64
    ];
65
66
    /**
67
     * The components that make up a select clause.
68
     *
69
     * @var array
70
     */
71
    protected $selectComponents = [
72
        'variables',
73
        'from',
74
        'search',
75
        'joins',
76
        // insert variables?
77
        'wheres',
78
        'groups',
79
        'aggregate',
80
        'havings',
81
        'orders',
82
        'offset',
83
        'limit',
84
        'columns',
85
    ];
86 49
87
    protected $operatorTranslations = [
88 49
        '='          => '==',
89
        '<>'         => '!=',
90
        '<=>'        => '==',
91
        'rlike'      => '=~',
92
        'not rlike'  => '!~',
93
        'regexp'     => '=~',
94
        'not regexp' => '!~',
95
    ];
96 12
97
    protected $whereTypeOperators = [
98 12
        'In'    => 'IN',
99
        'NotIn' => 'NOT IN',
100
    ];
101 162
102
    /**
103 162
     * The grammar specific bitwise operators.
104
     *
105
     * @var array
106
     */
107
    public $bitwiseOperators = [
108
        '&', '|', '^', '<<', '>>', '~',
109
    ];
110
111
    /**
112
     * Get the format for database stored dates.
113
     *
114
     * @return string
115
     */
116 44
    public function getDateFormat()
117
    {
118 44
        return 'Y-m-d\TH:i:s.vp';
119 1
    }
120
121 44
    /**
122
     * Get the grammar specific operators.
123 44
     *
124
     * @return array
125
     */
126
    public function getOperators()
127
    {
128
        return $this->operators;
129
    }
130 44
131 44
132
    public function translateOperator(string $operator): string
133
    {
134 44
        if (array_key_exists($operator, $this->operatorTranslations)) {
135 44
            return $this->operatorTranslations[$operator];
136 44
        }
137 44
138
        return $operator;
139 44
    }
140
141
    protected function prefixTable($table)
142
    {
143
        return $this->tablePrefix . $table;
144
    }
145
146
147 10
    /**
148
     * Get the appropriate query parameter place-holder for a value.
149 10
     *
150 10
     * @param  mixed  $value
151
     * @return string
152 10
     */
153
    public function parameter($value)
154 10
    {
155 9
156
        return $this->isExpression($value) ? $this->getValue($value) : $value;
157
    }
158 10
159
    /**
160
     * Compile the components necessary for a select clause.
161
     *
162
     * @param  IlluminateQueryBuilder  $query
163
     * @return array
164
     */
165
    protected function compileComponents(IlluminateQueryBuilder $query)
166 10
    {
167 10
        $aql = [];
168
169
        foreach ($this->selectComponents as $component) {
170 10
            if ($component === 'unions') {
171 10
                continue;
172 10
            }
173 10
174
            if (isset($query->$component)) {
175 10
                $method = 'compile' . ucfirst($component);
176
177
                $aql[$component] = $this->$method($query, $query->$component);
178
            }
179
        }
180
181
        return $aql;
182
    }
183
184
185 98
    /**
186
     * Compile an insert statement into AQL.
187 98
     *
188 98
     * @param IlluminateQueryBuilder $query
189
     * @param array   $values
190 98
     *
191
     * @throws BindException
192 98
     *
193
     * @return string
194
     */
195
    public function compileInsert(Builder|IlluminateQueryBuilder $query, array $values, string $bindVar = null)
196
    {
197
        $table = $this->prefixTable($query->from);
198
199 98
        if (empty($values)) {
200 98
            $aql = /** @lang AQL */ 'INSERT {} INTO ' . $table . ' RETURN NEW._key';
201
202
            return $aql;
203 98
        }
204 98
205 98
        return /** @lang AQL */ 'LET values = ' . $bindVar
206 98
                . ' FOR value IN values'
207 98
                . ' INSERT value INTO ' . $table
208
                . ' RETURN NEW._key';
209 98
    }
210
211
    /**
212
     * Compile an insert and get ID statement into SQL.
213
     *
214
     * @param array<mixed> $values
215
     */
216
    public function compileInsertGetId(IlluminateQueryBuilder $query, $values, $sequence = '_key', string $bindVar = null)
217
    {
218
        $table = $this->prefixTable($query->from);
219
220 146
        $sequence = $this->convertIdToKey($sequence);
221
222
        if (empty($values)) {
223
            $aql = /** @lang AQL */ 'INSERT {} INTO ' . $table . ' RETURN NEW.' . $sequence;
224
225
            return $aql;
226
        }
227
228
        $aql = /** @lang AQL */ 'LET values = ' . $bindVar
229
            . ' FOR value IN values'
230 146
            . ' INSERT value INTO ' . $table
231
            . ' RETURN NEW.' . $sequence;
232
233
        return $aql;
234
    }
235
236 146
    /**
237
     * Compile an insert statement into AQL.
238
     *
239
     * @param IlluminateQueryBuilder $query
240
     * @param array<mixed> $values
241
     * @return string
242
     */
243
    public function compileInsertOrIgnore(IlluminateQueryBuilder $query, array $values, string $bindVar = null)
244
    {
245 1
        $table = $this->prefixTable($query->from);
246
247
        if (empty($values)) {
248 1
            $aql = /** @lang AQL */ "INSERT {} INTO $table RETURN NEW._key";
249 1
250 1
            return $aql;
251
        }
252
253
        $aql = /** @lang AQL */ "LET values = $bindVar "
254
            . "FOR value IN values "
255
            . "INSERT value INTO $table "
256
            . "OPTIONS { ignoreErrors: true } "
257
            . "RETURN NEW._key";
258
259
        return $aql;
260 146
    }
261
262 146
    /**
263
     * Compile a select query into SQL.
264
     *
265
     * @param IlluminateQueryBuilder $query
266
     * @return string
267 146
     */
268 146
    public function compileSelect(IlluminateQueryBuilder $query)
269
    {
270 146
        // If the query does not have any columns set, we'll set the columns to the
271
        // * character to just get all of the columns from the database. Then we
272
        // can build the query and concatenate all the pieces together as one.
273
        $original = $query->columns;
274 146
275
        if (is_null($query->columns)) {
0 ignored issues
show
introduced by
The condition is_null($query->columns) is always false.
Loading history...
276
            $query->columns = ['*'];
277
        }
278
279
        // To compile the query, we'll spin through each component of the query and
280
        // see if that component exists. If it does we'll just call the compiler
281
        // function for the component which is responsible for making the SQL.
282
283
        $aql = trim(
284
            $this->concatenate(
285 146
                $this->compileComponents($query)
286
            )
287 146
        );
288 146
289
        //        if ($query->unions && $query->aggregate) {
290 146
        //            return $this->compileUnionAggregate($query);
291
        //        }
292 146
        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...
293
            return $this->compileUnions($query, $aql);
294
        }
295
296
        $query->columns = $original;
297
298
        return $aql;
299
    }
300 146
301
    /**
302 146
     * Compile a truncate table statement into SQL.
303 1
     *
304 1
     * @param  IlluminateQueryBuilder  $query
305
     * @return array
306
     */
307
    public function compileTruncate(IlluminateQueryBuilder $query)
308 146
    {
309
        return [$this->compileDelete($query) => []];
310
    }
311
312
    /**
313
     * Compile the "from" portion of the query -> FOR in AQL.
314
     *
315
     * @param IlluminateQueryBuilder $query
316
     * @param string  $table
317
     *
318
     * @return string
319 3
     */
320
    protected function compileFrom(IlluminateQueryBuilder $query, $table)
321 3
    {
322 3
        assert($query instanceof Builder);
323 3
324
        // FIXME: wrapping/quoting
325 3
        $table = $this->prefixTable($table);
326
327
        //FIXME: register given alias (x AS y in SQL)
328
        $alias = $query->registerTableAlias($table);
329
330
331
        return "FOR $alias IN $table";
332
    }
333
334
    /**
335
     * @param IlluminateQueryBuilder $query
336
     * @param array $variables
337
     * @return string
338
     */
339 3
    protected function compileVariables(IlluminateQueryBuilder $query, array $variables): string
340
    {
341 3
        $aql = "";
342
        foreach ($variables as $variable => $value) {
343 3
            if ($value instanceof Expression) {
344 3
                $value = $value->getValue($this);
345 2
            }
346
347
            $aql .= ' LET ' . $variable . ' = ' . $value;
348 3
        }
349
350 3
        return trim($aql);
351 2
    }
352
353
    /**
354
     * Compile the "order by" portions of the query.
355 3
     *
356
     * @param  \Illuminate\Database\Query\Builder  $query
357
     * @param  array  $orders
358
     * @return string
359
     */
360
    protected function compileOrders(IlluminateQueryBuilder $query, $orders)
361
    {
362
        if (!empty($orders)) {
363
            return 'SORT ' . implode(', ', $this->compileOrdersToArray($query, $orders));
364
        }
365
366
        return '';
367 4
    }
368
369 4
    /**
370
     * Compile the query orders to an array.
371 4
     *
372
     * @param  \Illuminate\Database\Query\Builder  $query
373
     * @param  array  $orders
374
     * @return array
375
     */
376
    protected function compileOrdersToArray(IlluminateQueryBuilder $query, $orders)
377
    {
378
        return array_map(function ($order) use ($query) {
379
            $key = 'column';
380
            if (array_key_exists('sql', $order)) {
381
                $key = 'sql';
382 72
            }
383
384 72
            if ($order[$key] instanceof Expression) {
385 4
                $order[$key] = $order[$key]->getValue($this);
386
            } else {
387 4
                $order[$key] = $this->normalizeColumn($query, $order[$key]);
388
            }
389 68
390
            return array_key_exists('direction', $order) ? $order[$key] . ' ' . $order['direction'] : $order[$key];
391 68
        }, $orders);
392
    }
393
394
    /**
395
     * Compile the "offset" portions of the query.
396
     *
397
     * @param  \Illuminate\Database\Query\Builder  $query
398
     * @param  int  $offset
399
     * @return string
400
     */
401
    protected function compileOffset(IlluminateQueryBuilder $query, $offset)
402
    {
403 23
        $this->offset = (int) $offset;
404
405
        return "";
406 23
    }
407 23
408
    /**
409 23
     * Compile the "limit" portions of the query.
410
     *
411
     * @param  \Illuminate\Database\Query\Builder  $query
412 23
     * @param  int  $limit
413
     * @return string
414 23
     */
415
    protected function compileLimit(IlluminateQueryBuilder $query, $limit)
416 23
    {
417
        if ($this->offset !== null) {
418
            return "LIMIT " . (int) $this->offset . ", " . (int) $limit;
419
        }
420
421
        return "LIMIT " . (int) $limit;
422
    }
423
424
    protected function createUpdateObject($values)
425
    {
426
        $valueStrings = [];
427
        foreach($values as $key => $value) {
428
            if (is_array($value)) {
429
                $valueStrings[] = $key . ': ' . $this->createUpdateObject($value);
430 1
            } else {
431
                $valueStrings[] = $key . ': ' . $value;
432
            }
433 1
        }
434 1
435
        return '{ ' . implode(', ', $valueStrings) . ' }';
436
    }
437 1
438 1
    /**
439
     * Compile an update statement into AQL.
440
     *
441 1
     * @param  \Illuminate\Database\Query\Builder  $query
442 1
     * @param  array  $values
443
     * @return string
444
     */
445
    public function compileUpdate(IlluminateQueryBuilder $query, array|string $values)
446 1
    {
447 1
        assert($query instanceof Builder);
448 1
449 1
        $table = $query->from;
450 1
        $alias = $query->getTableAlias($query->from);
451 1
452
        if (!is_array($values)) {
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
453 1
            $values = Arr::wrap($values);
454
        }
455
456
        $updateValues = $this->generateAqlObject($values);
457
458
        $aqlElements = [];
459
        $aqlElements[] = $this->compileFrom($query, $query->from);
460
461
        if (isset($query->joins)) {
462
            $aqlElements[] = $this->compileJoins($query, $query->joins);
463
        }
464
465
        $aqlElements[] = $this->compileWheres($query);
466
467 12
        $aqlElements[] = 'UPDATE ' . $alias . ' WITH ' . $updateValues . ' IN ' . $table;
468
469 12
        return implode(' ', $aqlElements);
470 12
    }
471
472
    /**
473 12
     * Compile an "upsert" statement into AQL.
474 1
     *
475
     * @param  \Illuminate\Database\Query\Builder  $query
476 1
     * @param  array  $values
477
     * @param  array  $uniqueBy
478
     * @param  array  $update
479 12
     * @return string
480
     */
481
    public function compileUpsert(IlluminateQueryBuilder $query, array $values, array $uniqueBy, array $update)
482 12
    {
483
        $searchFields = [];
484 12
        foreach($uniqueBy as $key => $field) {
485
            $searchFields[$field] = 'doc.' . $field;
486 12
        }
487
        $searchObject = $this->generateAqlObject($searchFields);
488
489
        $updateFields = [];
490
        foreach($update as $key => $field) {
491
            $updateFields[$field] = 'doc.' . $field;
492
        }
493
        $updateObject = $this->generateAqlObject($updateFields);
494
495
        $valueObjects = [];
496 1
        foreach($values as $data) {
497
            $valueObjects[] = $this->generateAqlObject($data);
498 1
        }
499
500
        return 'LET docs = [' . implode(', ', $valueObjects) . ']'
501
            . ' FOR doc IN docs'
502
            . ' UPSERT ' . $searchObject
503
            . ' INSERT doc'
504
            . ' UPDATE ' . $updateObject
505 7
            . ' IN ' . $query->from;
506
    }
507 7
508
    /**
509 7
     * Compile a delete statement into SQL.
510 3
     *
511
     * @param  \Illuminate\Database\Query\Builder  $query
512
     * @return string
513 7
     */
514
    public function compileDelete(IlluminateQueryBuilder $query)
515
    {
516
        $table = $query->from;
517
518
        $where = $this->compileWheres($query);
519
520
        return trim(
521
            isset($query->joins)
522 1
                ? $this->compileDeleteWithJoins($query, $table, $where)
523
                : $this->compileDeleteWithoutJoins($query, $table, $where)
524 1
        );
525
    }
526
527
528
    /**
529
     * Compile a delete statement without joins into SQL.
530
     *
531
     * @param  \Illuminate\Database\Query\Builder  $query
532
     * @param  string  $table
533
     * @param  string  $where
534
     * @return string
535
     */
536
    protected function compileDeleteWithoutJoins(IlluminateQueryBuilder $query, $table, $where)
537
    {
538
        assert($query instanceof Builder);
539
540
        $alias = $this->normalizeColumn($query, $query->registerTableAlias($table));
541
542
        $table = $this->wrapTable($this->prefixTable($table));
543
544
        return "FOR {$alias} IN {$table} {$where} REMOVE {$alias} IN {$table}";
545
    }
546
547
    /**
548
     * Compile the random statement into SQL.
549
     *
550
     * @param  string|int|null  $seed
551
     * @return string
552
     */
553
    public function compileRandom($seed = null)
554
    {
555
        unset($seed);
556
557
        return "RAND()";
558
    }
559
560
    /**
561
     * @param IlluminateQueryBuilder $query
562
     * @return string
563
     * @throws \Exception
564
     */
565
    public function compileSearch(IlluminateQueryBuilder $query, array $search)
566
    {
567
        $predicates = [];
568
        foreach($search['fields'] as $field) {
569
            $predicates[] = $this->normalizeColumn($query, $field)
570
                . ' IN TOKENS(' . $search['searchText'] . ', "text_en")';
571
        }
572
573
        return 'SEARCH ANALYZER('
574
            . implode(' OR ', $predicates)
575
            . ', "text_en")';
576
    }
577
578
    /**
579
     * Get the value of a raw expression.
580
     *
581
     * @param  \Illuminate\Database\Query\Expression  $expression
582
     * @return string
583
     */
584
    public function getValue($expression)
585
    {
586
        return $expression->getValue($this);
587
    }
588
589
    /**
590
     * Get the grammar specific bit operators.
591
     *
592
     * @return array
593
     */
594
    public function getBitwiseOperators()
595
    {
596
        return $this->bitwiseOperators;
597
    }
598
599
    /**
600
     * Prepare the bindings for a delete statement.
601
     *
602
     * @param  array  $bindings
603
     * @return array
604
     */
605
    public function prepareBindingsForDelete(array $bindings)
606
    {
607
        return Arr::collapse(
608
            Arr::except($bindings, 'select')
609
        );
610
    }
611
612
    /**
613
     * Determine if the given string is a JSON selector.
614
     *
615
     * @param  string  $value
616
     * @return bool
617
     */
618
    public function isJsonSelector($value)
619
    {
620
        if(!is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
621
            return false;
622
        }
623
624
        return str_contains($value, '->');
625
    }
626
627
    public function convertJsonFields($data): array|string
628
    {
629
        if (!is_array($data) && !is_string($data)) {
630
            return $data;
631
        }
632
633
        if (is_string($data)) {
634
            return str_replace('->', '.', $data);
635
        }
636
637
        if (array_is_list($data)) {
638
            return $this->convertJsonValuesToDotNotation($data);
639
        }
640
641
        return $this->convertJsonKeysToDotNotation($data);
642
    }
643
644
    public function convertJsonValuesToDotNotation(array $fields): array
645
    {
646
        foreach($fields as $key => $value) {
647
            if ($this->isJsonSelector($value)) {
648
                $fields[$key] = str_replace('->', '.', $value);
649
            }
650
        }
651
        return $fields;
652
    }
653
654
    public function convertJsonKeysToDotNotation(array $fields): array
655
    {
656
        foreach($fields as $key => $value) {
657
            if ($this->isJsonSelector($key)) {
658
                $fields[str_replace('->', '.', $key)] = $value;
659
                unset($fields[$key]);
660
            }
661
        }
662
        return $fields;
663
    }
664
}
665