Grammar::compileInsert()   B
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 8
nop 2
dl 0
loc 33
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: admin
5
 * Date: 21.01.2018
6
 * Time: 14:35
7
 */
8
9
namespace sonrac\Arango\Query\Grammars;
10
11
use Illuminate\Database\Query\Builder;
12
use Illuminate\Database\Query\Grammars\Grammar as IlluminateGrammar;
13
use function sonrac\Arango\Helpers\getEntityName;
14
use function sonrac\Arango\Helpers\getEntityNameFromColumn;
15
16
class Grammar extends IlluminateGrammar
17
{
18
    /**
19
     * The components that make up a select clause.
20
     *
21
     * @var array
22
     */
23
    protected $selectComponents = [
24
        'aggregate',
25
        'from',
26
        'joins',
27
        'wheres',
28
        'groups',
29
        'havings',
30
        'orders',
31
        'limit',
32
        'offset',
33
        'columns',
34
        'unions',
35
        'lock',
36
    ];
37
38
    /**
39
     * {@inheritdoc}
40
     */
41
    public function compileInsertGetId(Builder $query, $values, $sequence)
42
    {
43
        return $this->compileInsert($query, $values) . ' RETURN NEW';
44
    }
45
46
    /**
47
     * {@inheritdoc}
48
     */
49
    public function compileInsert(Builder $query, array $values)
50
    {
51
        // Essentially we will force every insert to be treated as a batch insert which
52
        // simply makes creating the SQL easier for us since we can utilize the same
53
        // basic routine regardless of an amount of records given to us to insert.
54
        $collection = $this->wrapTable($query->from);
55
56
        $entityName = getEntityName($query->from);
57
58
        if (!is_array(reset($values))) {
59
            $values = [$values];
60
        }
61
62
        $columns = array_keys(reset($values));
63
64
        $parameters = [];
65
66
        foreach ($values as $record) {
67
            $bindValuesTmp = [];
68
            foreach ($columns as $column) {
69
                if (!isset($record[$column])) {
70
                    continue;
71
                }
72
                $bindValuesTmp[$column] = $record[$column];
73
            }
74
            $parameters[] = $bindValuesTmp;
75
        }
76
        $parameters = json_encode($parameters);
77
        $parameters = preg_replace('/"(\@B\w+)"/', '$1', $parameters);
78
79
        $aql = "FOR {$entityName} IN {$parameters} INSERT {$entityName} INTO {$collection}";
80
        return $aql;
81
    }
82
83
    /**
84
     * Prepare column before use in AQL request
85
     * Add entityName and wrap it if needed.
86
     * Add alias for string like (column as other_name)
87
     *
88
     * @param $collection
89
     * @param $column
90
     * @param bool $withCollection
91
     * @return string
92
     */
93
    public function wrapColumn($column, $collection = null, $withCollection = true)
94
    {
95
        $entityName = getEntityNameFromColumn($column);
96
97
        $clearColumn = $this->getClearColumnName($column);
98
99
        $alias = $this->getAliasNameFromColumn($column);
100
101
        if (is_null($entityName) && !is_null($collection)) {
102
            $entityName = getEntityName($collection);
103
        }
104
105
        if ($clearColumn !== '_key') {
106
            $clearColumn = trim($clearColumn, '`');
107
            $clearColumn = '`'.$clearColumn.'`';
108
        }
109
        if ($withCollection) {
110
            $column = $entityName.'.'.$clearColumn;
111
        }
112
        if ($alias) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $alias of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
113
            $column = $alias.':'.$column;
114
        }
115
        return $column;
116
    }
117
118
    /**
119
     * Return collection name
120
     * @return string
121
     */
122
    public function wrapTable($table)
123
    {
124
        return $this->wrapCollection($table);
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130
    public function columnize(array $columns)
131
    {
132
        $resultColumns = [];
133
        foreach ($columns as $column) {
134
            if (strpos($column, ':') !== false) {
135
                $resultColumns[] = $column;
136
                continue;
137
            }
138
139
            list($entityName, $column) = explode('.', $column);
140
            if ($column === '`*`') {
141
                $resultColumns[] = $entityName . ': '.$entityName;
142
                continue;
143
            }
144
            $resultColumns[] = $column . ': ' . $entityName . '.' . $column;
145
        }
146
        return implode(',', $resultColumns);
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function compileUpdate(Builder $query, $values)
153
    {
154
        $table = $this->wrapTable($query->from);
155
156
        // Each one of the columns in the update statements needs to be wrapped in the
157
        // keyword identifiers, also a place-holder needs to be created for each of
158
        // the values in the list of bindings so we can make the sets statements.
159
        $columns = collect($values)->map(function ($value, $key) {
160
            return $key.' : '.$value;
161
        })->implode(', ');
162
163
        $joins = '';
164
165
        if (isset($query->joins)) {
166
            $joins = $this->compileJoins($query, $query->joins).' ';
167
        }
168
169
        // Of course, update queries may also be constrained by where clauses so we'll
170
        // need to compile the where clauses and attach it to the query so only the
171
        // intended records are updated by the SQL statements we generate to run.
172
        $wheres = $this->compileWheres($query);
173
174
        $entityName = getEntityName($table);
175
176
        $aql = $joins.'FOR '.$entityName.' IN '.$table.' '.$wheres.
177
            ' UPDATE '.$entityName.' WITH { '.$columns.' } IN '.$table;
178
179
        var_dump($aql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($aql); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
180
        return $aql;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function compileDelete(Builder $query)
187
    {
188
        $wheres = is_array($query->wheres) ? $this->compileWheres($query) : '';
189
190
        $collection = $this->wrapTable($query->from);
191
        $entityName = getEntityName($collection);
192
        $aql = "FOR {$entityName} in {$collection} {$wheres} REMOVE {$entityName} IN {$collection}";
193
        var_dump($aql);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($aql); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
194
        return $aql;
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function compileSelect(Builder $query)
201
    {
202
        // If the query does not have any columns set, we'll set the columns to the
203
        // * character to just get all of the columns from the database. Then we
204
        // can build the query and concatenate all the pieces together as one.
205
        $original = $query->columns;
206
207
        if (is_null($query->columns)) {
208
            $query->columns = ['*'];
209
        }
210
211
        // To compile the query, we'll spin through each component of the query and
212
        // see if that component exists. If it does we'll just call the compiler
213
        // function for the component which is responsible for making the AQL.
214
        $aql = trim($this->concatenate(
215
            $this->compileComponents($query))
216
        );
217
218
        if (isset($query->joins)) {
219
            $aql = $this->compileJoins($query, $query->joins).' '.$aql;
220
        }
221
222
        if (!is_null($query->aggregate)) {
223
            $aql = $this->compileAggregateExtended($query, $query->aggregate, $aql);
224
        }
225
        $query->columns = $original;
226
227
        return $aql;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    protected function compileJoins(Builder $query, $joins)
234
    {
235
        return collect($joins)->map(function ($join) use (&$aql) {
236
            $table = $this->wrapTable($join->table);
237
            $entityName = getEntityName($join->table);
238
            return 'FOR '.$entityName.' IN '.$table;
239
        })->implode(' ');
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    protected function compileComponents(Builder $query)
246
    {
247
        $aql = [];
248
249
        foreach ($this->selectComponents as $component) {
250
            // To compile the query, we'll spin through each component of the query and
251
            // see if that component exists. If it does we'll just call the compiler
252
            // function for the component which is responsible for making the SQL.
253
            if (!is_null($query->$component)) {
254
                if ($component === 'aggregate' ||
255
                   $component === 'joins') {
256
                    continue;
257
                }
258
                $method = 'compile'.ucfirst($component);
259
260
                $aql[$component] = $this->$method($query, $query->$component);
261
            }
262
        }
263
264
        return $aql;
265
    }
266
267
    /**
268
     * {@inheritdoc}
269
     */
270
    protected function compileFrom(Builder $query, $collection)
271
    {
272
        return 'FOR '.getEntityName($collection).' IN '.$this->wrapCollection($collection);
273
    }
274
275
    /**
276
     * {@inheritdoc}
277
     */
278
    protected function compileColumns(Builder $query, $columns)
279
    {
280
        if (count($columns) === 1 && $columns[0] === '*') {
281
            return 'RETURN '.getEntityName($query->from);
282
        }
283
284
        return 'RETURN { ' . $this->columnize($columns) . ' }';
285
    }
286
287
    /**
288
     * Return string for aggregate some column from AQL request
289
     *
290
     * @param Builder $query
291
     * @param $aggregate
292
     * @param $aql
293
     * @return string
294
     */
295
    protected function compileAggregateExtended(Builder $query, $aggregate, $aql)
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
296
    {
297
        return 'RETURN {"aggregate":'.$aggregate['function'].'('.$aql.')}';
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303
    protected function compileWheres(Builder $query)
304
    {
305
        // Each type of where clauses has its own compiler function which is responsible
306
        // for actually creating the where clauses SQL. This helps keep the code nice
307
        // and maintainable since each clause has a very small method that it uses.
308
        if (is_null($query->wheres)) {
309
            return '';
310
        }
311
312
        // If we actually have some where clauses, we will strip off the first boolean
313
        // operator, which is added by the query builders for convenience so we can
314
        // avoid checking for the first clauses in each of the compilers methods.
315
        if (count($sql = $this->compileWheresToArray($query)) > 0) {
316
            return $this->concatenateWhereClauses($query, $sql);
317
        }
318
319
        return '';
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    protected function concatenateWhereClauses($query, $sql)
326
    {
327
        return 'FILTER '.$this->removeLeadingBoolean(implode(' ', $sql));
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333
    protected function compileWheresToArray($query)
334
    {
335
        return collect($query->wheres)->map(function ($where) use ($query) {
336
            return $where['boolean'].' '.$this->{"where{$where['type']}"}($query, $where);
337
        })->all();
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343
    protected function whereBasic(Builder $query, $where)
344
    {
345
        return $where['column'].' '.$where['operator'].' '.$where['value'];
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     */
351 View Code Duplication
    protected function whereIn(Builder $query, $where)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
352
    {
353
        if (!empty($where['values'])) {
354
            $column = $this->wrapColumn($where['column'], $query->from);
355
            return '['.implode(',', $where['values']).'] ANY == '.$column;
356
        }
357
358
        return '0 = 1';
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364 View Code Duplication
    protected function whereNotIn(Builder $query, $where)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
365
    {
366
        if (!empty($where['values'])) {
367
            $column = $this->wrapColumn($where['table'], $where['column']);
368
            return '['.implode(',', $where['values']).'] NONE == '.$column;
369
        }
370
371
        return '0 = 1';
372
    }
373
374
    /**
375
     * {@inheritdoc}
376
     */
377
    protected function whereNull(Builder $query, $where)
378
    {
379
        return $this->wrapColumn($where['column'], $query->from).' == NULL';
380
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385
    protected function whereNotNull(Builder $query, $where)
386
    {
387
        return $this->wrapColumn($where['column'], $query->from).' != NULL';
388
    }
389
390
    /**
391
     * {@inheritdoc}
392
     */
393
    protected function whereColumn(Builder $query, $where)
394
    {
395
        $firstWrapColumn = $this->wrapColumn($where['first'], $query->from);
396
        $secondWrapColumn = $this->wrapColumn($where['second'], $query->from);
397
        return $firstWrapColumn.' '.$where['operator'].' '.$secondWrapColumn;
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403
    protected function compileOrders(Builder $query, $orders)
404
    {
405
        if (!empty($orders)) {
406
            return 'SORT '.implode(', ', $this->compileOrdersToArray($query, $orders));
407
        }
408
409
        return '';
410
    }
411
412
    /**
413
     * {@inheritdoc}
414
     */
415
    protected function compileOrdersToArray(Builder $query, $orders)
416
    {
417
        return array_map(function ($order) {
418
            return !isset($order['sql'])
419
                ? $order['column'].' '.$order['direction']
420
                : $order['sql'];
421
        }, $orders);
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     */
427
    protected function compileLimit(Builder $query, $limit)
428
    {
429
        $result = 'LIMIT ';
430
431
        if (isset($query->offset)) {
432
            $result .= (int) $query->offset.', ';
433
        }
434
        $result .= (int) $limit;
435
436
        return $result;
437
    }
438
439
    /**
440
     * {@inheritdoc}
441
     */
442
    protected function compileOffset(Builder $query, $offset)
443
    {
444
        if (!isset($query->limit)) {
445
            throw new \Exception("You can't set offset without limit for arangodb");
446
        }
447
        return '';
448
    }
449
450
    /**
451
     * Wrap collection name
452
     * @param string $collection
453
     * @return string
454
     */
455
    protected function wrapCollection($collection)
456
    {
457
        return '`'.trim($collection, '`').'`';
458
    }
459
460
    protected function getClearColumnName($column)
461
    {
462
        $parts = explode('.', $column);
463
        if (count($parts) > 1) {
464
            $column = $parts[1];
465
        }
466
        $column = explode('as', $column)[0];
467
468
        return trim($column, '` ');
469
    }
470
471
    protected function getAliasNameFromColumn($column)
472
    {
473
        $parts = explode('as', $column);
474
        if (count($parts) < 2) {
475
            return null;
476
        }
477
478
        return '`'.trim($parts[1], '` ').'`';
479
    }
480
}
481