Passed
Push — 2.x ( e6f7bd...b3cc5a )
by Aleksei
26:07 queued 06:51
created

SelectQuery::getIterator()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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