1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LaravelFreelancerNL\Aranguent\Query; |
4
|
|
|
|
5
|
|
|
use Closure; |
6
|
|
|
use Illuminate\Database\Query\Builder as IlluminateQueryBuilder; |
7
|
|
|
use Illuminate\Database\Query\Expression; |
8
|
|
|
use InvalidArgumentException; |
9
|
|
|
use LaravelFreelancerNL\Aranguent\Connection; |
10
|
|
|
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsJoinClauses; |
11
|
|
|
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsSubqueries; |
12
|
|
|
use LaravelFreelancerNL\Aranguent\Query\Concerns\BuildsWhereClauses; |
13
|
|
|
use LaravelFreelancerNL\FluentAQL\Exceptions\BindException; |
14
|
|
|
use LaravelFreelancerNL\FluentAQL\Expressions\ExpressionInterface as ExpressionInterface; |
15
|
|
|
use LaravelFreelancerNL\FluentAQL\Expressions\FunctionExpression; |
16
|
|
|
use LaravelFreelancerNL\FluentAQL\QueryBuilder; |
17
|
|
|
|
18
|
|
|
class Builder extends IlluminateQueryBuilder |
19
|
|
|
{ |
20
|
|
|
use BuildsJoinClauses; |
21
|
|
|
use BuildsSubqueries; |
22
|
|
|
use BuildsWhereClauses; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var QueryBuilder|FunctionExpression |
26
|
|
|
*/ |
27
|
|
|
public QueryBuilder|FunctionExpression $aqb; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var Connection |
31
|
|
|
*/ |
32
|
|
|
public $connection; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var Grammar |
36
|
|
|
*/ |
37
|
|
|
public $grammar; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* The current query value bindings. |
41
|
|
|
* |
42
|
|
|
* @var null|array{predicates: array<mixed>, options: array<string, string>} |
43
|
|
|
*/ |
44
|
|
|
public ?array $search = null; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* The query variables that should be set. |
48
|
|
|
* |
49
|
|
|
* @var array<mixed> |
50
|
|
|
*/ |
51
|
|
|
public $variables = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @override |
55
|
|
|
* Create a new query builder instance. |
56
|
|
|
*/ |
57
|
158 |
|
public function __construct( |
58
|
|
|
Connection $connection, |
59
|
|
|
Grammar $grammar = null, |
60
|
|
|
Processor $processor = null, |
61
|
|
|
QueryBuilder $aqb = null |
62
|
|
|
) { |
63
|
158 |
|
$this->connection = $connection; |
64
|
158 |
|
$this->grammar = $grammar ?: $connection->getQueryGrammar(); |
|
|
|
|
65
|
158 |
|
$this->processor = $processor ?: $connection->getPostProcessor(); |
66
|
158 |
|
if (!$aqb instanceof QueryBuilder) { |
67
|
158 |
|
$aqb = new QueryBuilder(); |
68
|
|
|
} |
69
|
158 |
|
$this->aqb = $aqb; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Delete a record from the database. |
74
|
|
|
* |
75
|
|
|
* @param mixed $id |
76
|
|
|
* |
77
|
|
|
* @return int |
78
|
|
|
*/ |
79
|
10 |
|
public function delete($id = null) |
80
|
|
|
{ |
81
|
10 |
|
$this->aqb = new QueryBuilder(); |
82
|
10 |
|
$this->grammar->compileDelete($this, $id)->setAql(); |
83
|
10 |
|
return $this->connection->delete($this->aqb); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Set the table which the query is targeting. |
88
|
|
|
* |
89
|
|
|
* @param \Closure|IlluminateQueryBuilder|string $table |
90
|
|
|
* @param string|null $as |
91
|
|
|
* @return IlluminateQueryBuilder |
92
|
|
|
*/ |
93
|
158 |
|
public function from($table, $as = null) |
94
|
|
|
{ |
95
|
158 |
|
if ($this->isQueryable($table)) { |
96
|
|
|
return $this->fromSub($table, $as); |
97
|
|
|
} |
98
|
158 |
|
$this->grammar->registerTableAlias($table, $as); |
|
|
|
|
99
|
|
|
|
100
|
158 |
|
$this->from = $table; |
|
|
|
|
101
|
|
|
|
102
|
158 |
|
return $this; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Run the query as a "select" statement against the connection. |
107
|
|
|
* |
108
|
|
|
* @return array |
109
|
|
|
*/ |
110
|
117 |
|
protected function runSelect() |
111
|
|
|
{ |
112
|
117 |
|
$this->aqb = new QueryBuilder(); |
113
|
117 |
|
$this->grammar->compileSelect($this)->setAql(); |
114
|
117 |
|
$results = $this->connection->select($this->aqb); |
115
|
116 |
|
$this->aqb = new QueryBuilder(); |
116
|
116 |
|
return $results; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Run a pagination count query. |
121
|
|
|
* |
122
|
|
|
* @param array $columns |
123
|
|
|
* |
124
|
|
|
* @return array |
125
|
|
|
*/ |
126
|
5 |
|
protected function runPaginationCountQuery($columns = ['*']) |
127
|
|
|
{ |
128
|
5 |
|
$without = $this->unions ? ['orders', 'limit', 'offset'] : ['columns', 'orders', 'limit', 'offset']; |
129
|
|
|
|
130
|
5 |
|
$closeResults = $this->cloneWithout($without) |
131
|
5 |
|
->cloneWithoutBindings($this->unions ? ['order'] : ['select', 'order']) |
132
|
5 |
|
->setAggregate('count', $this->withoutSelectAliases($columns)) |
133
|
|
|
->get()->all(); |
134
|
|
|
|
135
|
5 |
|
return $closeResults; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* Set the columns to be selected. |
140
|
|
|
* |
141
|
|
|
* @param array|mixed $columns |
142
|
|
|
* @return IlluminateQueryBuilder |
143
|
|
|
*/ |
144
|
53 |
|
public function select($columns = ['*']) |
145
|
|
|
{ |
146
|
53 |
|
$this->columns = []; |
147
|
53 |
|
$this->bindings['select'] = []; |
148
|
53 |
|
$columns = is_array($columns) ? $columns : func_get_args(); |
149
|
|
|
|
150
|
53 |
|
$this->addColumns($columns); |
151
|
|
|
|
152
|
53 |
|
return $this; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Add a new select column to the query. |
157
|
|
|
* |
158
|
|
|
* @param array|mixed $column |
159
|
|
|
* @return $this |
160
|
|
|
*/ |
161
|
13 |
|
public function addSelect($column) |
162
|
|
|
{ |
163
|
13 |
|
$columns = is_array($column) ? $column : func_get_args(); |
164
|
|
|
|
165
|
13 |
|
$this->addColumns($columns); |
166
|
|
|
|
167
|
13 |
|
return $this; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* @param array<mixed> $columns |
172
|
|
|
*/ |
173
|
66 |
|
protected function addColumns(array $columns): void |
174
|
|
|
{ |
175
|
66 |
|
foreach ($columns as $as => $column) { |
176
|
66 |
|
if (is_string($as) && $this->isQueryable($column)) { |
177
|
|
|
if (is_null($this->columns)) { |
178
|
|
|
$this->select($this->from . '.*'); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$this->selectSub($column, $as); |
182
|
|
|
|
183
|
|
|
continue; |
184
|
|
|
} |
185
|
|
|
|
186
|
66 |
|
if (! is_string($as) || ! $this->isQueryable($column)) { |
187
|
66 |
|
$this->columns[$as] = $column; |
188
|
|
|
|
189
|
66 |
|
continue; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$this->columns[] = $column; |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Get the SQL representation of the query. |
198
|
|
|
* |
199
|
|
|
* @return string |
200
|
|
|
*/ |
201
|
40 |
|
public function toSql() |
202
|
|
|
{ |
203
|
40 |
|
$this->grammar->compileSelect($this)->setAql(); |
204
|
40 |
|
return $this->aqb->query; |
|
|
|
|
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Insert a new record into the database. |
209
|
|
|
* |
210
|
|
|
* @param array $values |
211
|
|
|
* |
212
|
|
|
* @throws BindException |
213
|
|
|
* |
214
|
|
|
* @return bool |
215
|
|
|
*/ |
216
|
9 |
|
public function insert(array $values): bool |
217
|
|
|
{ |
218
|
9 |
|
$this->grammar->compileInsert($this, $values)->setAql(); |
219
|
9 |
|
$results = $this->getConnection()->insert($this->aqb); |
|
|
|
|
220
|
9 |
|
$this->aqb = new QueryBuilder(); |
221
|
9 |
|
return $results; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Insert a new record and get the value of the primary key. |
226
|
|
|
* |
227
|
|
|
* @param array<mixed> $values |
228
|
|
|
*/ |
229
|
13 |
|
public function insertGetId(array $values, $sequence = null) |
230
|
|
|
{ |
231
|
13 |
|
$this->grammar->compileInsertGetId($this, $values, $sequence)->setAql(); |
232
|
13 |
|
$response = $this->getConnection()->execute($this->aqb); |
|
|
|
|
233
|
13 |
|
$this->aqb = new QueryBuilder(); |
234
|
|
|
|
235
|
13 |
|
return (is_array($response)) ? end($response) : $response; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Insert a new record into the database. |
240
|
|
|
* |
241
|
|
|
* @param array $values |
242
|
|
|
* |
243
|
|
|
* @throws BindException |
244
|
|
|
* |
245
|
|
|
* @return bool |
246
|
|
|
*/ |
247
|
4 |
|
public function insertOrIgnore(array $values): bool |
248
|
|
|
{ |
249
|
4 |
|
$this->grammar->compileInsertOrIgnore($this, $values)->setAql(); |
250
|
4 |
|
$results = $this->getConnection()->insert($this->aqb); |
|
|
|
|
251
|
4 |
|
$this->aqb = new QueryBuilder(); |
252
|
|
|
|
253
|
4 |
|
return $results; |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
/** |
258
|
|
|
* Get the current query value bindings in a flattened array. |
259
|
|
|
* |
260
|
|
|
* @return array |
261
|
|
|
*/ |
262
|
10 |
|
public function getBindings() |
263
|
|
|
{ |
264
|
10 |
|
return $this->aqb->binds; |
|
|
|
|
265
|
|
|
} |
266
|
|
|
|
267
|
156 |
|
protected function setAql() |
268
|
|
|
{ |
269
|
156 |
|
$this->aqb = $this->aqb->get(); |
|
|
|
|
270
|
|
|
|
271
|
156 |
|
return $this; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Update a record in the database. |
276
|
|
|
* |
277
|
|
|
* @param array $values |
278
|
|
|
* |
279
|
|
|
* @return int |
280
|
|
|
*/ |
281
|
23 |
|
public function update(array $values) |
282
|
|
|
{ |
283
|
23 |
|
$this->aqb = new QueryBuilder(); |
284
|
|
|
|
285
|
23 |
|
$this->grammar->compileUpdate($this, $values)->setAql(); |
286
|
23 |
|
$results = $this->connection->update($this->aqb); |
287
|
23 |
|
$this->aqb = new QueryBuilder(); |
288
|
23 |
|
return $results; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Execute an aggregate function on the database. |
293
|
|
|
* |
294
|
|
|
* @param string $function |
295
|
|
|
* @param array $columns |
296
|
|
|
* |
297
|
|
|
* @return mixed |
298
|
|
|
*/ |
299
|
19 |
|
public function aggregate($function, $columns = ['*']) |
300
|
|
|
{ |
301
|
19 |
|
$results = $this->cloneWithout($this->unions ? [] : ['columns']) |
302
|
|
|
->setAggregate($function, $columns) |
303
|
|
|
->get($columns); |
304
|
|
|
|
305
|
19 |
|
$this->aqb = new QueryBuilder(); |
306
|
|
|
|
307
|
19 |
|
if (!$results->isEmpty()) { |
308
|
19 |
|
return array_change_key_case((array) $results[0])['aggregate']; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
return false; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Determine if the given operator is supported. |
317
|
|
|
* |
318
|
|
|
* @param string $operator |
319
|
|
|
* |
320
|
|
|
* @return bool |
321
|
|
|
*/ |
322
|
94 |
|
protected function invalidOperator($operator) |
323
|
|
|
{ |
324
|
94 |
|
return !in_array(strtolower($operator), $this->operators, true) && |
325
|
94 |
|
!isset($this->grammar->getOperators()[strtoupper($operator)]); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Add an "order by" clause to the query. |
331
|
|
|
* |
332
|
|
|
* @param Closure|IlluminateQueryBuilder|string $column |
333
|
|
|
* @param string $direction |
334
|
|
|
* |
335
|
|
|
* @throws InvalidArgumentException |
336
|
|
|
* |
337
|
|
|
* @return $this |
338
|
|
|
*/ |
339
|
2 |
|
public function orderBy($column, $direction = 'asc') |
340
|
|
|
{ |
341
|
2 |
|
if ($this->isQueryable($column)) { |
342
|
|
|
[$query, $bindings] = $this->createSub($column); |
343
|
|
|
|
344
|
|
|
//fixme: Remove binding when implementing subqueries |
345
|
|
|
$bindings = null; |
|
|
|
|
346
|
|
|
|
347
|
|
|
$column = new Expression('(' . $query . ')'); |
348
|
|
|
} |
349
|
|
|
|
350
|
2 |
|
$this->{$this->unions ? 'unionOrders' : 'orders'}[] = [ |
351
|
|
|
'column' => $column, |
352
|
|
|
'direction' => $direction, |
353
|
|
|
]; |
354
|
|
|
|
355
|
2 |
|
return $this; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Add a raw "order by" clause to the query. |
360
|
|
|
* |
361
|
|
|
* @param string|ExpressionInterface $aql |
362
|
|
|
* @param array $bindings |
363
|
|
|
* |
364
|
|
|
* @return $this |
365
|
|
|
*/ |
366
|
1 |
|
public function orderByRaw($aql, $bindings = []) |
367
|
|
|
{ |
368
|
1 |
|
$bindings = []; |
|
|
|
|
369
|
|
|
|
370
|
1 |
|
$type = 'Raw'; |
371
|
1 |
|
$column = $aql; |
372
|
1 |
|
$this->{$this->unions ? 'unionOrders' : 'orders'}[] = compact('type', 'column'); |
373
|
|
|
|
374
|
1 |
|
return $this; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
|
378
|
|
|
/** |
379
|
|
|
* Put the query's results in random order. |
380
|
|
|
* |
381
|
|
|
* @param string $seed |
382
|
|
|
* |
383
|
|
|
* @return $this |
384
|
|
|
*/ |
385
|
1 |
|
public function inRandomOrder($seed = '') |
386
|
|
|
{ |
387
|
|
|
// ArangoDB's random function doesn't accept a seed. |
388
|
1 |
|
$seed = null; |
|
|
|
|
389
|
|
|
|
390
|
1 |
|
return $this->orderByRaw($this->grammar->compileRandom($this)); |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
/** |
394
|
|
|
* Search an ArangoSearch view. |
395
|
|
|
* |
396
|
|
|
* @param mixed $predicates |
397
|
|
|
* @param array|null $options |
398
|
|
|
* @return Builder |
399
|
|
|
*/ |
400
|
7 |
|
public function search(mixed $predicates, array $options = null): Builder |
401
|
|
|
{ |
402
|
7 |
|
if ($predicates instanceof Closure) { |
403
|
2 |
|
$predicates = $predicates($this->aqb); |
404
|
|
|
} |
405
|
|
|
|
406
|
7 |
|
if (! is_array($predicates)) { |
407
|
4 |
|
$predicates = [$predicates]; |
408
|
|
|
} |
409
|
|
|
|
410
|
7 |
|
$this->search = [ |
411
|
|
|
'predicates' => $predicates, |
412
|
|
|
'options' => $options |
413
|
|
|
]; |
414
|
|
|
|
415
|
7 |
|
return $this; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* Create a new query instance for a sub-query. |
420
|
|
|
* |
421
|
|
|
* @return $this |
422
|
|
|
*/ |
423
|
6 |
|
protected function forSubQuery() |
424
|
|
|
{ |
425
|
6 |
|
return $this->newQuery(); |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* Get the database connection instance. |
430
|
|
|
* |
431
|
|
|
* @return Connection |
432
|
|
|
*/ |
433
|
94 |
|
public function getConnection() |
434
|
|
|
{ |
435
|
94 |
|
return $this->connection; |
436
|
|
|
} |
437
|
|
|
} |
438
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.