Failed Conditions
Push — refactor/improve-static-analys... ( bdf823...46faab )
by Bas
10:08
created

Grammar::translateOperator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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 = 'INSERT {} INTO ' . $table . ' RETURN NEW._key';
201
202
            return $aql;
203 98
        }
204 98
205 98
        return '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 = 'INSERT {} INTO ' . $table . ' RETURN NEW.' . $sequence;
224
225
            return $aql;
226
        }
227
228
        $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 = "INSERT {} INTO $table RETURN NEW._key";
249 1
250 1
            return $aql;
251
        }
252
253
        $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 Builder
319 3
     */
320
    protected function compileFrom(IlluminateQueryBuilder $query, $table)
321 3
    {
322 3
        // FIXME: wrapping/quoting
323 3
        $table = $this->prefixTable($table);
324
325 3
        //FIXME: register given alias (x AS y in SQL)
326
        $alias = $query->registerTableAlias($table);
327
328
329
        return "FOR $alias IN $table";
0 ignored issues
show
Bug Best Practice introduced by
The expression return 'FOR '.$alias.' IN '.$table returns the type string which is incompatible with the documented return type LaravelFreelancerNL\Aranguent\Query\Builder.
Loading history...
330
    }
331
332
    /**
333
     * @param IlluminateQueryBuilder $query
334
     * @param array $variables
335
     * @return string
336
     */
337
    protected function compileVariables(IlluminateQueryBuilder $query, array $variables): string
338
    {
339 3
        $aql = "";
340
        foreach ($variables as $variable => $value) {
341 3
            if ($value instanceof Expression) {
342
                $value = $value->getValue($this);
343 3
            }
344 3
345 2
            $aql .= ' LET ' . $variable . ' = ' . $value;
346
        }
347
348 3
        return trim($aql);
349
    }
350 3
351 2
    /**
352
     * Compile the "order by" portions of the query.
353
     *
354
     * @param  \Illuminate\Database\Query\Builder  $query
355 3
     * @param  array  $orders
356
     * @return string
357
     */
358
    protected function compileOrders(IlluminateQueryBuilder $query, $orders)
359
    {
360
        if (!empty($orders)) {
361
            return 'SORT ' . implode(', ', $this->compileOrdersToArray($query, $orders));
362
        }
363
364
        return '';
365
    }
366
367 4
    /**
368
     * Compile the query orders to an array.
369 4
     *
370
     * @param  \Illuminate\Database\Query\Builder  $query
371 4
     * @param  array  $orders
372
     * @return array
373
     */
374
    protected function compileOrdersToArray(IlluminateQueryBuilder $query, $orders)
375
    {
376
        return array_map(function ($order) use ($query) {
377
            $key = 'column';
378
            if (array_key_exists('sql', $order)) {
379
                $key = 'sql';
380
            }
381
382 72
            if ( $order[$key] instanceof Expression) {
383
                $order[$key] = $order[$key]->getValue($this);
384 72
            } else {
385 4
                $order[$key] = $this->normalizeColumn($query, $order[$key]);
386
            }
387 4
388
            return array_key_exists('direction', $order) ? $order[$key].' '.$order['direction'] : $order[$key];
389 68
        }, $orders);
390
    }
391 68
392
    /**
393
     * Compile the "offset" portions of the query.
394
     *
395
     * @param  \Illuminate\Database\Query\Builder  $query
396
     * @param  int  $offset
397
     * @return string
398
     */
399
    protected function compileOffset(IlluminateQueryBuilder $query, $offset)
400
    {
401
        $this->offset = (int) $offset;
402
403 23
        return "";
404
    }
405
406 23
    /**
407 23
     * Compile the "limit" portions of the query.
408
     *
409 23
     * @param  \Illuminate\Database\Query\Builder  $query
410
     * @param  int  $limit
411
     * @return string
412 23
     */
413
    protected function compileLimit(IlluminateQueryBuilder $query, $limit)
414 23
    {
415
        if ($this->offset !== null) {
416 23
            return "LIMIT " . (int) $this->offset . ", " . (int) $limit;
417
        }
418
419
        return "LIMIT " . (int) $limit;
420
    }
421
422
    protected function createUpdateObject($values)
423
    {
424
        $valueStrings = [];
425
        foreach($values as $key => $value) {
426
            if (is_array($value)) {
427
                $valueStrings[] = $key . ': ' . $this->createUpdateObject($value);
428
            } else {
429
                $valueStrings[] = $key . ': ' . $value;
430 1
            }
431
        }
432
433 1
        return '{ ' . implode(', ', $valueStrings) . ' }';
434 1
    }
435
436
    /**
437 1
     * Compile an update statement into AQL.
438 1
     *
439
     * @param  \Illuminate\Database\Query\Builder  $query
440
     * @param  array  $values
441 1
     * @return string
442 1
     */
443
    public function compileUpdate(IlluminateQueryBuilder $query, array|string $values)
444
    {
445
        $table = $query->from;
446 1
        $alias = $query->getTableAlias($query->from);
447 1
448 1
        if (!is_array($values)) {
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
449 1
            $values = Arr::wrap($values);
450 1
        }
451 1
452
        $updateValues = $this->generateAqlObject($values);
453 1
454
        $aqlElements = [];
455
        $aqlElements[] = $this->compileFrom($query, $query->from);
456
457
        if (isset($query->joins)) {
458
            $aqlElements[] = $this->compileJoins($query, $query->joins);
459
        }
460
461
        $aqlElements[] = $this->compileWheres($query);
462
463
        $aqlElements[] = 'UPDATE ' . $alias . ' WITH ' . $updateValues . ' IN ' . $table;
464
465
        return implode(' ', $aqlElements);
466
    }
467 12
468
    /**
469 12
     * Compile an "upsert" statement into AQL.
470 12
     *
471
     * @param  \Illuminate\Database\Query\Builder  $query
472
     * @param  array  $values
473 12
     * @param  array  $uniqueBy
474 1
     * @param  array  $update
475
     * @return string
476 1
     */
477
    public function compileUpsert(IlluminateQueryBuilder $query, array $values, array $uniqueBy, array $update)
478
    {
479 12
        $searchFields = [];
480
        foreach($uniqueBy as $key => $field) {
481
            $searchFields[$field] = 'doc.' . $field;
482 12
        }
483
        $searchObject = $this->generateAqlObject($searchFields);
484 12
485
        $updateFields = [];
486 12
        foreach($update as $key => $field) {
487
            $updateFields[$field] = 'doc.' . $field;
488
        }
489
        $updateObject = $this->generateAqlObject($updateFields);
490
491
        $valueObjects = [];
492
        foreach($values as $data) {
493
            $valueObjects[] = $this->generateAqlObject($data);
494
        }
495
496 1
        return 'LET docs = [' . implode(', ', $valueObjects) . ']'
497
            . ' FOR doc IN docs'
498 1
            . ' UPSERT ' . $searchObject
499
            . ' INSERT doc'
500
            . ' UPDATE ' . $updateObject
501
            . ' IN ' . $query->from;
502
    }
503
504
    /**
505 7
     * Compile a delete statement into SQL.
506
     *
507 7
     * @param  \Illuminate\Database\Query\Builder  $query
508
     * @return string
509 7
     */
510 3
    public function compileDelete(IlluminateQueryBuilder $query)
511
    {
512
        $table = $query->from;
513 7
514
        $where = $this->compileWheres($query);
515
516
        return trim(
517
            isset($query->joins)
518
                ? $this->compileDeleteWithJoins($query, $table, $where)
519
                : $this->compileDeleteWithoutJoins($query, $table, $where)
520
        );
521
    }
522 1
523
524 1
    /**
525
     * Compile a delete statement without joins into SQL.
526
     *
527
     * @param  \Illuminate\Database\Query\Builder  $query
528
     * @param  string  $table
529
     * @param  string  $where
530
     * @return string
531
     */
532
    protected function compileDeleteWithoutJoins(IlluminateQueryBuilder $query, $table, $where)
533
    {
534
535
        $alias = $this->normalizeColumn($query, $query->registerTableAlias($table));
536
537
        $table = $this->wrapTable($this->prefixTable($table));
538
539
        return "FOR {$alias} IN {$table} {$where} REMOVE {$alias} IN {$table}";
540
    }
541
542
    /**
543
     * Compile the random statement into SQL.
544
     *
545
     * @param  string|int|null  $seed
546
     * @return string
547
     */
548
    public function compileRandom($seed = null)
549
    {
550
        unset($seed);
551
552
        return "RAND()";
553
    }
554
555
    /**
556
     * @param IlluminateQueryBuilder $query
557
     * @return string
558
     * @throws \Exception
559
     */
560
    public function compileSearch(IlluminateQueryBuilder $query, array $search)
561
    {
562
        $predicates = [];
563
        foreach($search['fields'] as $field) {
564
            $predicates[] = $this->normalizeColumn($query, $field)
565
                . ' IN TOKENS(' . $search['searchText'] . ', "text_en")';
566
        }
567
568
        return 'SEARCH ANALYZER('
569
            . implode(' OR ', $predicates)
570
            . ', "text_en")';
571
    }
572
573
    /**
574
     * Get the value of a raw expression.
575
     *
576
     * @param  \Illuminate\Database\Query\Expression  $expression
577
     * @return string
578
     */
579
    public function getValue($expression)
580
    {
581
        return $expression->getValue($this);
582
    }
583
584
    /**
585
     * Get the grammar specific bit operators.
586
     *
587
     * @return array
588
     */
589
    public function getBitwiseOperators()
590
    {
591
        return $this->bitwiseOperators;
592
    }
593
594
    /**
595
     * Prepare the bindings for a delete statement.
596
     *
597
     * @param  array  $bindings
598
     * @return array
599
     */
600
    public function prepareBindingsForDelete(array $bindings)
601
    {
602
        return Arr::collapse(
603
            Arr::except($bindings, 'select')
604
        );
605
    }
606
607
    /**
608
     * Determine if the given string is a JSON selector.
609
     *
610
     * @param  string  $value
611
     * @return bool
612
     */
613
    public function isJsonSelector($value)
614
    {
615
        if(!is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
616
            return false;
617
        }
618
619
        return str_contains($value, '->');
620
    }
621
622
    public function convertJsonFields($data): array|string
623
    {
624
        if (!is_array($data) && !is_string($data)) {
625
            return $data;
626
        }
627
628
        if (is_string($data)) {
629
            return str_replace('->', '.', $data);
630
        }
631
632
        if (array_is_list($data)) {
633
            return $this->convertJsonValuesToDotNotation($data);
634
        }
635
636
        return $this->convertJsonKeysToDotNotation($data);
637
    }
638
639
    public function convertJsonValuesToDotNotation(array $fields): array
640
    {
641
        foreach($fields as $key => $value) {
642
            if ($this->isJsonSelector($value)) {
643
                $fields[$key] = str_replace('->', '.', $value);
644
            }
645
        }
646
        return $fields;
647
    }
648
649
    public function convertJsonKeysToDotNotation(array $fields): array
650
    {
651
        foreach($fields as $key => $value) {
652
            if ($this->isJsonSelector($key)) {
653
                $fields[str_replace('->', '.', $key)] = $value;
654
                unset($fields[$key]);
655
            }
656
        }
657
        return $fields;
658
    }
659
}
660