Completed
Branch feature/pre-split (4c50c1)
by Anton
03:17
created

SelectQuery::getColumns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Spiral Framework.
4
 *
5
 * @license   MIT
6
 * @author    Anton Titov (Wolfy-J)
7
 */
8
9
namespace Spiral\Database\Builders;
10
11
use Spiral\Database\Builders\Prototypes\AbstractSelect;
12
use Spiral\Database\Entities\Driver;
13
use Spiral\Database\Entities\QueryCompiler;
14
use Spiral\Database\Entities\QueryStatement;
15
use Spiral\Database\Exceptions\BuilderException;
16
use Spiral\Database\Exceptions\QueryException;
17
use Spiral\Database\Injections\FragmentInterface;
18
use Spiral\Debug\Traits\LoggerTrait;
19
20
/**
21
 * SelectQuery extends AbstractSelect with ability to specify selection tables and perform UNION
22
 * of multiple select queries.
23
 */
24
class SelectQuery extends AbstractSelect implements \JsonSerializable
25
{
26
    use LoggerTrait;
27
28
    /**
29
     * Table names to select data from.
30
     *
31
     * @var array
32
     */
33
    protected $tables = [];
34
35
    /**
36
     * Select queries represented by sql fragments or query builders to be united. Stored as
37
     * [UNION TYPE, SELECT QUERY].
38
     *
39
     * @var array
40
     */
41
    protected $unionTokens = [];
42
43
    /**
44
     * {@inheritdoc}
45
     *
46
     * @param array $from    Initial set of table names.
47
     * @param array $columns Initial set of columns to fetch.
48
     */
49
    public function __construct(
50
        Driver $driver,
51
        QueryCompiler $compiler,
52
        array $from = [],
53
        array $columns = []
54
    ) {
55
        parent::__construct($driver, $compiler);
56
57
        $this->tables = $from;
58
        if (!empty($columns)) {
59
            $this->columns = $this->fetchIdentifiers($columns);
60
        }
61
    }
62
63
    /**
64
     * Set table names SELECT query should be performed for. Table names can be provided with
65
     * specified alias (AS construction).
66
     *
67
     * @param array|string|mixed $tables Array of names, comma separated string or set of
68
     *                                   parameters.
69
     *
70
     * @return self|$this
71
     */
72
    public function from($tables): SelectQuery
73
    {
74
        $this->tables = $this->fetchIdentifiers(func_get_args());
75
76
        return $this;
77
    }
78
79
    /**
80
     * Tables to be loaded.
81
     *
82
     * @return array
83
     */
84
    public function getTables(): array
85
    {
86
        return $this->tables;
87
    }
88
89
    /**
90
     * Set columns should be fetched as result of SELECT query. Columns can be provided with
91
     * specified alias (AS construction).
92
     *
93
     * @param array|string|mixed $columns Array of names, comma separated string or set of
94
     *                                    parameters.
95
     *
96
     * @return self|$this
97
     */
98
    public function columns($columns): SelectQuery
99
    {
100
        $this->columns = $this->fetchIdentifiers(func_get_args());
101
102
        return $this;
103
    }
104
105
    /**
106
     * Set of columns to be selected.
107
     *
108
     * @return array
109
     */
110
    public function getColumns(): array
111
    {
112
        return $this->columns;
113
    }
114
115
    /**
116
     * Alias for columns() method.
117
     *
118
     * @param array|string|mixed $columns Array of names, comma separated string or set of
119
     *                                    parameters.
120
     *
121
     * @return self|$this
122
     */
123
    public function select($columns): SelectQuery
124
    {
125
        $this->columns = $this->fetchIdentifiers(func_get_args());
126
127
        return $this;
128
    }
129
130
    /**
131
     * Add select query to be united with.
132
     *
133
     * @param FragmentInterface $query
134
     *
135
     * @return self|$this
136
     */
137
    public function union(FragmentInterface $query): SelectQuery
138
    {
139
        $this->unionTokens[] = ['', $query];
140
141
        return $this;
142
    }
143
144
    /**
145
     * Add select query to be united with. Duplicate values will be included in result.
146
     *
147
     * @param FragmentInterface $query
148
     *
149
     * @return self|$this
150
     */
151
    public function unionAll(FragmentInterface $query): SelectQuery
152
    {
153
        $this->unionTokens[] = ['ALL', $query];
154
155
        return $this;
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function getParameters(): array
162
    {
163
        $parameters = parent::getParameters();
164
165
        //Unions always located at the end of query.
166
        foreach ($this->unionTokens as $union) {
167
            if ($union[1] instanceof QueryBuilder) {
168
                $parameters = array_merge($parameters, $union[1]->getParameters());
169
            }
170
        }
171
172
        return $parameters;
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     *
178
     * @param bool $paginate Apply pagination to result, can be disabled in honor of count method.
179
     *
180
     * @return QueryStatement
181
     */
182
    public function run(bool $paginate = true)
183
    {
184
        if ($paginate && $this->hasPaginator()) {
185
            /**
186
             * To prevent original select builder altering
187
             *
188
             * @var SelectQuery $select
189
             */
190
            $select = clone $this;
191
192
            //Getting selection specific paginator
193
            $paginator = $this->configurePaginator($this->count());
194
195
            if (!empty($this->getLimit()) && $this->getLimit() > $paginator->getLimit()) {
196
                //We have to ensure that selection works inside given pagination window
197
                $select = $select->limit($this->getLimit());
198
            } else {
199
                $select->limit($paginator->getLimit());
200
            }
201
202
            //Making sure that window is shifted
203
            $select = $select->offset($this->getOffset() + $paginator->getOffset());
204
205
            //No inner pagination
206
            return $select->run(false);
207
        }
208
209
        return $this->driver->query($this->sqlStatement(), $this->getParameters());
210
    }
211
212
    /**
213
     * Iterate thought result using smaller data chinks with defined size and walk function.
214
     *
215
     * Example:
216
     * $select->chunked(100, function(PDOResult $result, $offset, $count) {
217
     *      dump($result);
218
     * });
219
     *
220
     * You must return FALSE from walk function to stop chunking.
221
     *
222
     * @param int      $limit
223
     * @param callable $callback
224
     */
225
    public function runChunks(int $limit, callable $callback)
226
    {
227
        $count = $this->count();
228
229
        //To keep original query untouched
230
        $select = clone $this;
231
232
        $select->limit($limit);
233
234
        $offset = 0;
235
        while ($offset + $limit <= $count) {
236
            $result = call_user_func_array(
237
                $callback,
238
                [$select->offset($offset)->getIterator(), $offset, $count]
239
            );
240
241
            if ($result === false) {
242
                //Stop iteration
243
                return;
244
            }
245
246
            $offset += $limit;
247
        }
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     *
253
     * Count number of rows in query. Limit, offset, order by, group by values will be ignored. Do
254
     * not count united queries, or queries in complex joins.
255
     *
256
     * @param string $column Column to count by (every column by default).
257
     *
258
     * @return int
259
     */
260
    public function count(string $column = '*'): int
261
    {
262
        /**
263
         * @var AbstractSelect $select
264
         */
265
        $select = clone $this;
266
        $select->columns = ["COUNT({$column})"];
267
        $select->ordering = [];
268
        $select->grouping = [];
269
270
        return (int)$select->run(false)->fetchColumn();
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     *
276
     * Shortcut to execute one of aggregation methods (AVG, MAX, MIN, SUM) using method name as
277
     * reference.
278
     *
279
     * Example:
280
     * echo $select->sum('user.balance');
281
     *
282
     * @param string $method
283
     * @param array  $arguments
284
     *
285
     * @return int|float
286
     *
287
     * @throws BuilderException
288
     * @throws QueryException
289
     */
290
    public function __call(string $method, array $arguments)
291
    {
292
        if (!in_array($method = strtoupper($method), ['AVG', 'MIN', 'MAX', 'SUM'])) {
293
            throw new BuilderException("Unknown method '{$method}' in '" . get_class($this) . "'");
294
        }
295
296
        if (!isset($arguments[0]) || count($arguments) > 1) {
297
            throw new BuilderException('Aggregation methods can support exactly one column');
298
        }
299
300
        /**
301
         * @var AbstractSelect $select
302
         */
303
        $select = clone $this;
304
        $select->columns = ["{$method}({$arguments[0]})"];
305
306
        $result = $select->run(false)->fetchColumn();
307
308
        //Selecting type between int and float
309
        if ((float)$result == $result && (int)$result != $result) {
310
            //Find more elegant check
311
            return (float)$result;
312
        }
313
314
        return (int)$result;
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     *
320
     * @return \PDOStatement|QueryStatement
321
     */
322
    public function getIterator()
323
    {
324
        return $this->run();
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function sqlStatement(QueryCompiler $compiler = null): string
331
    {
332
        if (empty($compiler)) {
333
            $compiler = $this->compiler->resetQuoter();
334
        }
335
336
        if ((!empty($this->getLimit()) || !empty($this->getOffset())) && empty($this->ordering)) {
337
            $this->logger()->warning(
338
                "Usage of LIMIT/OFFSET without proper ORDER BY statement is ambiguous"
339
            );
340
        }
341
342
        //11 parameters!
343
        return $compiler->compileSelect(
344
            $this->tables,
345
            $this->distinct,
346
            $this->columns,
347
            $this->joinTokens,
348
            $this->whereTokens,
349
            $this->havingTokens,
350
            $this->grouping,
351
            $this->ordering,
352
            $this->getLimit(),
353
            $this->getOffset(),
354
            $this->unionTokens
355
        );
356
    }
357
358
    /**
359
     * {@inheritdoc}
360
     */
361
    public function jsonSerialize()
362
    {
363
        return $this->fetchAll();
364
    }
365
366
    /**
367
     * Request all results as array.
368
     *
369
     * @return array
370
     */
371
    public function fetchAll(): array
372
    {
373
        return $this->getIterator()->fetchAll(\PDO::FETCH_ASSOC);
374
    }
375
}
376