SelectQuery::offset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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