Passed
Pull Request — 2.x (#44)
by butschster
03:27
created

SelectQuery::max()   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 1
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 IteratorAggregate;
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
use Throwable;
27
28
/**
29
 * Builds select sql statements.
30
 */
31
class SelectQuery extends ActiveQuery implements
32
    Countable,
33
    IteratorAggregate,
34
    PaginableInterface
35
{
36
    use HavingTrait;
37
    use JoinTrait;
38
    use TokenTrait;
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 bool|string|array $distinct = false;
48
    protected array $columns = ['*'];
49
    /** @var FragmentInterface[][]|string[][] */
50
    protected array $orderBy = [];
51
    protected array $groupBy = [];
52
    protected bool $forUpdate = false;
53
54
    private ?int $limit = null;
55
    private ?int $offset = null;
56
57
    /**
58
     * @param array $from    Initial set of table names.
59
     * @param array $columns Initial set of columns to fetch.
60
     */
61 116
    public function __construct(array $from = [], array $columns = [])
62
    {
63 116
        $this->tables = $from;
64 116
        if ($columns !== []) {
65
            $this->columns = $this->fetchIdentifiers($columns);
66
        }
67 116
    }
68
69
    /**
70
     * Mark query to return only distinct results.
71
     *
72
     * @param bool|FragmentInterface|string $distinct You are only allowed to use string value for
73
     *                                                Postgres databases.
74
     */
75 234
    public function distinct(bool|string|FragmentInterface $distinct = true): self
76
    {
77 234
        $this->distinct = $distinct;
78
79 234
        return $this;
80
    }
81
82
    /**
83
     * Set table names SELECT query should be performed for. Table names can be provided with
84
     * specified alias (AS construction).
85
     */
86 1890
    public function from(mixed $tables): self
87
    {
88 1890
        $this->tables = $this->fetchIdentifiers(\func_get_args());
89
90 1890
        return $this;
91
    }
92
93 8
    public function getTables(): array
94
    {
95 8
        return $this->tables;
96
    }
97
98
    /**
99
     * Set columns should be fetched as result of SELECT query. Columns can be provided with
100
     * specified alias (AS construction).
101
     */
102 1890
    public function columns(mixed $columns): self
103
    {
104 1890
        $this->columns = $this->fetchIdentifiers(func_get_args());
105
106 1890
        return $this;
107
    }
108
109 8
    public function getColumns(): array
110
    {
111 8
        return $this->columns;
112
    }
113
114
    /**
115
     * Select entities for the following update.
116
     */
117 16
    public function forUpdate(): self
118
    {
119 16
        $this->forUpdate = true;
120
121 16
        return $this;
122
    }
123
124
    /**
125
     * Sort result by column/expression. You can apply multiple sortings to query via calling method
126
     * few times or by specifying values using array of sort parameters.
127
     *
128
     * $select->orderBy([
129
     *      'id'   => SelectQuery::SORT_DESC,
130
     *      'name' => SelectQuery::SORT_ASC
131
     * ]);
132
     *
133
     * @param 'ASC'|'DESC' $direction Sorting direction
0 ignored issues
show
Documentation Bug introduced by
The doc comment 'ASC'|'DESC' at position 0 could not be parsed: Unknown type name ''ASC'' at position 0 in 'ASC'|'DESC'.
Loading history...
134
     */
135
    public function orderBy(string|FragmentInterface|array $expression, string $direction = self::SORT_ASC): self
136 138
    {
137
        if (!\is_array($expression)) {
0 ignored issues
show
introduced by
The condition is_array($expression) is always true.
Loading history...
138 138
            $this->addOrder($expression, $direction);
139 130
            return $this;
140 130
        }
141
142
        foreach ($expression as $nested => $dir) {
143 16
            $this->addOrder($nested, $dir);
144 16
        }
145
146
        return $this;
147 16
    }
148
149
    /**
150
     * Column or expression to group query by.
151
     */
152
    public function groupBy(string|Fragment|Expression $expression): self
153 80
    {
154
        $this->groupBy[] = $expression;
155 80
156
        return $this;
157 80
    }
158
159
    /**
160
     * Add select query to be united with.
161
     */
162
    public function union(FragmentInterface $query): self
163 16
    {
164
        $this->unionTokens[] = ['', $query];
165 16
166
        return $this;
167 16
    }
168
169
    /**
170
     * Add select query to be united with. Duplicate values will be included in result.
171
     */
172
    public function unionAll(FragmentInterface $query): self
173 16
    {
174
        $this->unionTokens[] = ['ALL', $query];
175 16
176
        return $this;
177 16
    }
178
179
    /**
180
     * Set selection limit. Attention, this limit value does not affect values set in paginator but
181
     * only changes pagination window. Set to 0 to disable limiting.
182
     */
183
    public function limit(int $limit = null): self
184 92
    {
185
        $this->limit = $limit;
186 92
187
        return $this;
188 92
    }
189
190
    public function getLimit(): ?int
191
    {
192
        return $this->limit;
193
    }
194
195
    /**
196
     * Set selection offset. Attention, this value does not affect associated paginator but only
197
     * changes pagination window.
198
     */
199
    public function offset(int $offset = null): self
200 64
    {
201
        $this->offset = $offset;
202 64
203
        return $this;
204 64
    }
205
206
    public function getOffset(): ?int
207
    {
208
        return $this->offset;
209
    }
210
211
    public function run(): StatementInterface
212 400
    {
213
        $params = new QueryParameters();
214 400
        $queryString = $this->sqlStatement($params);
215 400
216
        return $this->driver->query($queryString, $params->getParameters());
217 400
    }
218
219
    /**
220
     * Iterate thought result using smaller data chinks with defined size and walk function.
221
     *
222
     * Example:
223
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
224
     *      dump($result);
225
     * });
226
     *
227
     * You must return FALSE from walk function to stop chunking.
228
     *
229
     * @throws Throwable
230
     */
231
    public function runChunks(int $limit, callable $callback): void
232 16
    {
233
        $count = $this->count();
234 16
235
        // to keep original query untouched
236
        $select = clone $this;
237 16
        $select->limit($limit);
238 16
239
        $offset = 0;
240 16
        while ($offset + $limit <= $count) {
241 16
            $result = $callback(
242 16
                $select->offset($offset)->getIterator(),
243 16
                $offset,
244
                $count
245
            );
246
247
            // stop iteration
248
            if ($result === false) {
249 16
                return;
250 8
            }
251
252
            $offset += $limit;
253 16
        }
254
    }
255 8
256
    /**
257
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored.
258
     *
259
     * @psalm-param non-empty-string $column Column to count by (every column by default).
260
     */
261
    public function count(string $column = '*'): int
262 262
    {
263
        $select = clone $this;
264 262
265
        //To be escaped in compiler
266
        $select->columns = ["COUNT({$column})"];
267 262
        $select->orderBy = [];
268 262
        $select->groupBy = [];
269 262
270
        $st = $select->run();
271 262
        try {
272
            return (int)$st->fetchColumn();
273 262
        } finally {
274
            $st->close();
275 262
        }
276
    }
277
278
    /**
279
     * @psalm-param non-empty-string $column
280
     */
281
    public function avg(string $column): mixed
282 18
    {
283
        return $this->runAggregate('AVG', $column);
284 18
    }
285
286
    /**
287
     * @psalm-param non-empty-string $column
288
     */
289
    public function max(string $column): mixed
290 8
    {
291
        return $this->runAggregate('MAX', $column);
292 8
    }
293
294
    /**
295
     * @psalm-param non-empty-string $column
296
     */
297
    public function min(string $column): mixed
298 8
    {
299
        return $this->runAggregate('MIN', $column);
300 8
    }
301
302
    /**
303
     * @psalm-param non-empty-string $column
304
     */
305
    public function sum(string $column): mixed
306 8
    {
307
        return $this->runAggregate('SUM', $column);
308 8
    }
309
310
    public function getIterator(): StatementInterface
311 98
    {
312
        return $this->run();
313 98
    }
314
315
    /**
316
     * Request all results as array.
317
     */
318
    public function fetchAll(int $mode = StatementInterface::FETCH_ASSOC): array
319 52
    {
320
        $st = $this->run();
321 52
        try {
322
            return $st->fetchAll($mode);
323 52
        } finally {
324
            $st->close();
325 52
        }
326
    }
327
328
    public function getType(): int
329 1794
    {
330
        return CompilerInterface::SELECT_QUERY;
331 1794
    }
332
333
    public function getTokens(): array
334 1794
    {
335
        return [
336
            'forUpdate' => $this->forUpdate,
337 1794
            'from'      => $this->tables,
338 1794
            'join'      => $this->joinTokens,
339 1794
            'columns'   => $this->columns,
340 1794
            'distinct'  => $this->distinct,
341 1794
            'where'     => $this->whereTokens,
342 1794
            'having'    => $this->havingTokens,
343 1794
            'groupBy'   => $this->groupBy,
344 1794
            'orderBy'   => array_values($this->orderBy),
345 1794
            'limit'     => $this->limit,
346 1794
            'offset'    => $this->offset,
347 1794
            'union'     => $this->unionTokens,
348 1794
        ];
349
    }
350
351
    /**
352
     * @param FragmentInterface|string $field
353
     * @param string                   $order Sorting direction, ASC|DESC.
354
     *
355
     * @return $this|self
356
     */
357
    private function addOrder(string|FragmentInterface $field, string $order): self
358 138
    {
359
        if (!\is_string($field)) {
360 138
            $this->orderBy[] = [$field, $order];
361 16
        } elseif (!\array_key_exists($field, $this->orderBy)) {
362 122
            $this->orderBy[$field] = [$field, $order];
363 122
        }
364
        return $this;
365 138
    }
366
367
    /**
368
     * @psalm-param non-empty-string $method
369
     * @psalm-param non-empty-string $column
370
     */
371
    private function runAggregate(string $method, string $column): mixed
372 42
    {
373
        $select = clone $this;
374 42
375
        //To be escaped in compiler
376
        $select->columns = ["{$method}({$column})"];
377 42
378
        $st = $select->run();
379 42
        try {
380
            return $st->fetchColumn();
381 42
        } finally {
382
            $st->close();
383 42
        }
384
    }
385
}
386