Issues (265)

src/Query/SelectQuery.php (3 issues)

1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Query;
13
14
use Cycle\Database\Injection\Expression;
15
use Cycle\Database\Injection\Fragment;
16
use Cycle\Database\Injection\SubQuery;
17
use Cycle\Database\Query\Traits\WhereJsonTrait;
18
use Cycle\Database\Driver\CompilerInterface;
19
use Cycle\Database\Injection\FragmentInterface;
20
use Cycle\Database\Query\Traits\HavingTrait;
21
use Cycle\Database\Query\Traits\JoinTrait;
22
use Cycle\Database\Query\Traits\TokenTrait;
23
use Cycle\Database\Query\Traits\WhereTrait;
24
use Cycle\Database\StatementInterface;
25
use Spiral\Pagination\PaginableInterface;
26
27
/**
28
 * Builds select sql statements.
29
 */
30
class SelectQuery extends ActiveQuery implements
31
    \Countable,
32
    \IteratorAggregate,
33
    PaginableInterface
34
{
35
    use HavingTrait;
36
    use JoinTrait;
37
    use TokenTrait;
38
    use WhereJsonTrait;
39
    use WhereTrait;
40
41
    // sort directions
42
    public const SORT_ASC = 'ASC';
43
    public const SORT_DESC = 'DESC';
44
45
    protected array $tables = [];
46
    protected array $unionTokens = [];
47
    protected array $exceptTokens = [];
48
    protected array $intersectTokens = [];
49
    protected bool|string|array $distinct = false;
50
    protected array $columns = ['*'];
51
52
    /** @var FragmentInterface[][]|string[][] */
53
    protected array $orderBy = [];
54
55
    protected array $groupBy = [];
56
    protected bool $forUpdate = false;
57
    private ?int $limit = null;
58
    private ?int $offset = null;
59
60
    /**
61 118
     * @param array $from Initial set of table names.
62
     * @param array $columns Initial set of columns to fetch.
63 118
     */
64 118
    public function __construct(array $from = [], array $columns = [])
65
    {
66
        $this->tables = $from;
67 118
        if ($columns !== []) {
68
            $this->columns = $this->fetchIdentifiers($columns);
69
        }
70
    }
71
72
    /**
73
     * Mark query to return only distinct results.
74
     *
75 234
     * @param bool|FragmentInterface|string $distinct You are only allowed to use string value for
76
     *                                                Postgres databases.
77 234
     */
78
    public function distinct(bool|string|FragmentInterface $distinct = true): self
79 234
    {
80
        $this->distinct = $distinct;
81
82
        return $this;
83
    }
84
85
    /**
86 1898
     * Set table names SELECT query should be performed for. Table names can be provided with
87
     * specified alias (AS construction).
88 1898
     * Also, it is possible to use SubQuery.
89
     *
90 1898
     * Following example will provide SQL like this: SELECT * FROM (SELECT * FROM users) AS u
91
     *
92
     * ```
93 8
     *  $subQuery = new SubQuery($queryBuilder->select()->from(['users']),'u');
94
     *  $query = $queryBuilder->select()->from($subQuery);
95 8
     * ```
96
     *
97
     * @see SubQuery
98
     */
99
    public function from(mixed $tables): self
100
    {
101
        $this->tables = $this->fetchIdentifiers(\func_get_args());
102 1898
103
        return $this;
104 1898
    }
105
106 1898
    public function getTables(): array
107
    {
108
        return $this->tables;
109 8
    }
110
111 8
    /**
112
     * Set columns should be fetched as result of SELECT query. Columns can be provided with
113
     * specified alias (AS construction).
114
     */
115
    public function columns(mixed $columns): self
116
    {
117 16
        $this->columns = $this->fetchIdentifiers(\func_get_args());
118
119 16
        return $this;
120
    }
121 16
122
    public function getColumns(): array
123
    {
124
        return $this->columns;
125
    }
126
127
    /**
128
     * Select entities for the following update.
129
     */
130
    public function forUpdate(): self
131
    {
132
        $this->forUpdate = true;
133
134
        return $this;
135 138
    }
136
137 138
    /**
138 130
     * Sort result by column/expression. You can apply multiple sortings to query via calling method
139 130
     * few times or by specifying values using array of sort parameters.
140
     *
141
     * $select->orderBy([
142 16
     *      'id'   => SelectQuery::SORT_DESC,
143 16
     *      'name' => SelectQuery::SORT_ASC,
144
     *
145
     *      // The following options below have the same effect (Direction will be ignored)
146 16
     *      new Fragment('RAND()') => null,
147
     *      new Fragment('RAND()')
148
     * ]);
149
     *
150
     * $select->orderBy('name', SelectQuery::SORT_ASC);
151
     *
152 80
     * $select->orderBy(new Fragment('RAND()'), null); // direction will be ignored
153
     * $select->orderBy(new Fragment('RAND()'), 'ASC NULLS LAST'); // Postgres specific directions are also supported
154 80
     *
155
     * @param 'ASC'|'DESC'|null $direction Sorting direction
0 ignored issues
show
Documentation Bug introduced by
The doc comment 'ASC'|'DESC'|null at position 0 could not be parsed: Unknown type name ''ASC'' at position 0 in 'ASC'|'DESC'|null.
Loading history...
156 80
     */
157
    public function orderBy(string|FragmentInterface|array $expression, ?string $direction = self::SORT_ASC): self
158
    {
159
        if (!\is_array($expression)) {
0 ignored issues
show
The condition is_array($expression) is always true.
Loading history...
160
            $this->addOrder($expression, $direction);
161
            return $this;
162 16
        }
163
164 16
        foreach ($expression as $nested => $dir) {
165
            // support for orderBy([new Fragment('RAND()')]) without passing direction
166 16
            if (\is_int($nested)) {
167
                $nested = $dir;
168
                $dir = null;
169
            }
170
171
            $this->addOrder($nested, $dir);
172 16
        }
173
174 16
        return $this;
175
    }
176 16
177
    /**
178
     * Column or expression to group query by.
179
     */
180
    public function groupBy(string|Fragment|Expression $expression): self
181
    {
182
        $this->groupBy[] = $expression;
183 92
184
        return $this;
185 92
    }
186
187 92
    /**
188
     * Add select query to be united with.
189
     */
190
    public function union(FragmentInterface $query): self
191
    {
192
        $this->unionTokens[] = ['', $query];
193
194
        return $this;
195
    }
196
197
    /**
198
     * Add select query to be united with. Duplicate values will be included in result.
199 64
     */
200
    public function unionAll(FragmentInterface $query): self
201 64
    {
202
        $this->unionTokens[] = ['ALL', $query];
203 64
204
        return $this;
205
    }
206
207
    /**
208
     * Add select query to be intersected with.
209
     */
210
    public function intersect(FragmentInterface $query): self
211 400
    {
212
        $this->intersectTokens[] = ['', $query];
213 400
214 400
        return $this;
215
    }
216 400
217
    /**
218
     * Add select query to be intersected with. Duplicate values will be included in result.
219
     */
220
    public function intersectAll(FragmentInterface $query): self
221
    {
222
        $this->intersectTokens[] = ['ALL', $query];
223
224
        return $this;
225
    }
226
227
    /**
228
     * Add select query to be excepted with.
229
     */
230
    public function except(FragmentInterface $query): self
231 16
    {
232
        $this->exceptTokens[] = ['', $query];
233 16
234
        return $this;
235
    }
236 16
237 16
    /**
238
     * Add select query to be excepted with. Duplicate values will be included in result.
239 16
     */
240 16
    public function exceptAll(FragmentInterface $query): self
241 16
    {
242 16
        $this->exceptTokens[] = ['ALL', $query];
243
244
        return $this;
245
    }
246
247
    /**
248 16
     * Set selection limit. Attention, this limit value does not affect values set in paginator but
249 8
     * only changes pagination window. Set to 0 to disable limiting.
250
     */
251
    public function limit(?int $limit = null): self
252 16
    {
253
        $this->limit = $limit;
254 8
255
        return $this;
256
    }
257
258
    public function getLimit(): ?int
259
    {
260
        return $this->limit;
261 262
    }
262
263 262
    /**
264
     * Set selection offset. Attention, this value does not affect associated paginator but only
265
     * changes pagination window.
266 262
     */
267 262
    public function offset(?int $offset = null): self
268 262
    {
269
        $this->offset = $offset;
270 262
271
        return $this;
272 262
    }
273
274 262
    public function getOffset(): ?int
275
    {
276
        return $this->offset;
277
    }
278
279
    public function run(): StatementInterface
280
    {
281 18
        $params = new QueryParameters();
282
        $queryString = $this->sqlStatement($params);
283 18
284
        return $this->driver->query($queryString, $params->getParameters());
0 ignored issues
show
The method query() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

284
        return $this->driver->/** @scrutinizer ignore-call */ query($queryString, $params->getParameters());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
285
    }
286
287
    /**
288
     * Iterate thought result using smaller data chinks with defined size and walk function.
289 8
     *
290
     * Example:
291 8
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
292
     *      dump($result);
293
     * });
294
     *
295
     * You must return FALSE from walk function to stop chunking.
296
     *
297 8
     * @throws \Throwable
298
     */
299 8
    public function runChunks(int $limit, callable $callback): void
300
    {
301
        $count = $this->count();
302
303
        // to keep original query untouched
304
        $select = clone $this;
305 8
        $select->limit($limit);
306
307 8
        $offset = 0;
308
        while ($offset + $limit <= $count) {
309
            $result = $callback(
310 98
                $select->offset($offset)->getIterator(),
311
                $offset,
312 98
                $count,
313
            );
314
315
            // stop iteration
316
            if ($result === false) {
317
                return;
318 52
            }
319
320 52
            $offset += $limit;
321
        }
322 52
    }
323
324 52
    /**
325
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored.
326
     *
327
     * @psalm-param non-empty-string $column Column to count by (every column by default).
328 1802
     */
329
    public function count(string $column = '*'): int
330 1802
    {
331
        $select = clone $this;
332
333 1802
        //To be escaped in compiler
334
        $select->columns = ["COUNT({$column})"];
335
        $select->orderBy = [];
336 1802
        $select->groupBy = [];
337 1802
338 1802
        $st = $select->run();
339 1802
        try {
340 1802
            return (int) $st->fetchColumn();
341 1802
        } finally {
342 1802
            $st->close();
343 1802
        }
344 1802
    }
345 1802
346 1802
    /**
347 1802
     * @psalm-param non-empty-string $column
348
     */
349
    public function avg(string $column): mixed
350
    {
351
        return $this->runAggregate('AVG', $column);
352
    }
353
354
    /**
355
     * @psalm-param non-empty-string $column
356
     */
357 138
    public function max(string $column): mixed
358
    {
359 138
        return $this->runAggregate('MAX', $column);
360 16
    }
361 122
362 122
    /**
363
     * @psalm-param non-empty-string $column
364 138
     */
365
    public function min(string $column): mixed
366
    {
367
        return $this->runAggregate('MIN', $column);
368
    }
369
370
    /**
371 42
     * @psalm-param non-empty-string $column
372
     */
373 42
    public function sum(string $column): mixed
374
    {
375
        return $this->runAggregate('SUM', $column);
376 42
    }
377
378 42
    public function getIterator(): StatementInterface
379
    {
380 42
        return $this->run();
381
    }
382 42
383
    /**
384
     * Request all results as array.
385
     */
386
    public function fetchAll(int $mode = StatementInterface::FETCH_ASSOC): array
387
    {
388
        $st = $this->run();
389
        try {
390
            return $st->fetchAll($mode);
391
        } finally {
392
            $st->close();
393
        }
394
    }
395
396
    public function getType(): int
397
    {
398
        return CompilerInterface::SELECT_QUERY;
399
    }
400
401
    public function getTokens(): array
402
    {
403
        return [
404
            'forUpdate' => $this->forUpdate,
405
            'from'      => $this->tables,
406
            'join'      => $this->joinTokens,
407
            'columns'   => $this->columns,
408
            'distinct'  => $this->distinct,
409
            'where'     => $this->whereTokens,
410
            'having'    => $this->havingTokens,
411
            'groupBy'   => $this->groupBy,
412
            'orderBy'   => \array_values($this->orderBy),
413
            'limit'     => $this->limit,
414
            'offset'    => $this->offset,
415
            'union'     => $this->unionTokens,
416
            'intersect' => $this->intersectTokens,
417
            'except'    => $this->exceptTokens,
418
        ];
419
    }
420
421
    /**
422
     * @param string|null $order Sorting direction, ASC|DESC|null.
423
     *
424
     * @return $this|self
425
     */
426
    private function addOrder(string|FragmentInterface $field, ?string $order): self
427
    {
428
        if (!\is_string($field)) {
429
            $this->orderBy[] = [$field, $order];
430
        } elseif (!\array_key_exists($field, $this->orderBy)) {
431
            $this->orderBy[$field] = [$field, $order];
432
        }
433
        return $this;
434
    }
435
436
    /**
437
     * @psalm-param non-empty-string $method
438
     * @psalm-param non-empty-string $column
439
     */
440
    private function runAggregate(string $method, string $column): mixed
441
    {
442
        $select = clone $this;
443
444
        //To be escaped in compiler
445
        $select->columns = ["{$method}({$column})"];
446
447
        $st = $select->run();
448
        try {
449
            return $st->fetchColumn();
450
        } finally {
451
            $st->close();
452
        }
453
    }
454
}
455