Completed
Push — master ( d82db7...107033 )
by Ivan
02:39
created

TableQuery::collection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
namespace vakata\database\schema;
3
4
use vakata\database\DBInterface;
5
use vakata\database\DBException;
6
use vakata\database\ResultInterface;
7
8
/**
9
 * A database query class
10
 */
11
class TableQuery implements \IteratorAggregate, \ArrayAccess, \Countable
12
{
13
    const SEP = '___';
14
    /**
15
     * @var DBInterface
16
     */
17
    protected $db;
18
    /**
19
     * @var Table
20
     */
21
    protected $definition;
22
    /**
23
     * @var TableQueryIterator|null
24
     */
25
    protected $qiterator;
26
27
    /**
28
     * @var array
29
     */
30
    protected $where = [];
31
    /**
32
     * @var array
33
     */
34
    protected $order = [];
35
    /**
36
     * @var array
37
     */
38
    protected $group = [];
39
    /**
40
     * @var array
41
     */
42
    protected $having = [];
43
    /**
44
     * @var int[]
45
     */
46
    protected $li_of = [0,0];
47
    /**
48
     * @var array
49
     */
50
    protected $fields = [];
51
    /**
52
     * @var array
53
     */
54
    protected $withr = [];
55
    /**
56
     * @var array
57
     */
58
    protected $joins = [];
59
60
    /**
61
     * Create an instance
62
     * @param  DBInterface    $db         the database connection
63
     * @param  Table|string   $table      the name or definition of the main table in the query
64
     */
65 13
    public function __construct(DBInterface $db, $table)
66
    {
67 13
        $this->db = $db;
68 13
        $this->definition = $table instanceof Table ? $table : $this->db->definition((string)$table);
69 13
        $this->columns($this->definition->getColumns());
70 13
    }
71
    public function __clone()
72
    {
73
        $this->reset();
74
    }
75
    /**
76
     * Get the table definition of the queried table
77
     * @return Table        the definition
78
     */
79
    public function getDefinition() : Table
80
    {
81
        return $this->definition;
82
    }
83
84 13
    protected function getColumn($column)
85
    {
86 13
        $column = explode('.', $column);
87 13
        if (count($column) === 1) {
88 13
            $column = [ $this->definition->getName(), $column[0] ];
89 13
            $col = $this->definition->getColumn($column[1]);
90 2
        } elseif (count($column) === 2) {
91 2
            if ($column[0] === $this->definition->getName()) {
92
                $col = $this->definition->getColumn($column[1]);
93
                if (!$col) {
94
                    throw new DBException('Invalid column name in own table');
95
                }
96
            } else {
97 2
                if ($this->definition->hasRelation($column[0])) {
98 1
                    $col = $this->definition->getRelation($column[0])->table->getColumn($column[1]);
99 1
                    if (!$col) {
100 1
                        throw new DBException('Invalid column name in related table');
101
                    }
102 1
                } else if (isset($this->joins[$column[0]])) {
103 1
                    $col = $this->joins[$column[0]]->table->getColumn($column[1]);
104 1
                    if (!$col) {
105 1
                        throw new DBException('Invalid column name in related table');
106
                    }
107
                } else {
108 2
                    throw new DBException('Invalid foreign table name: ' . implode(',', $column));
109
                }
110
            }
111
        } else {
112
            $name = array_pop($column);
113
            $this->with(implode('.', $column));
114
            $table = $this->definition;
115
            $table = array_reduce(
116
                $column,
117
                function ($carry, $item) use (&$table) {
118
                    $table = $table->getRelation($item)->table;
119
                    return $table;
120
                }
121
            );
122
            $col = $table->getColumn($name);
123
            $column = [ implode(static::SEP, $column), $name ];
124
        }
125 13
        return [ 'name' => implode('.', $column), 'data' => $col ];
126
    }
127 4
    protected function normalizeValue(TableColumn $col, $value)
128
    {
129 4
        if ($value === null && $col->isNullable()) {
130
            return null;
131
        }
132 4
        switch ($col->getBasicType()) {
133 4
            case 'date':
134
                if (is_string($value)) {
135
                    $temp = strtotime($value);
136
                    if (!$temp) {
137
                        return null;
138
                    }
139
                    return date('Y-m-d', strtotime($value));
140
                }
141
                if (is_int($value)) {
142
                    return date('Y-m-d', $value);
143
                }
144
                if ($value instanceof \DateTime) {
145
                    return $value->format('Y-m-d');
146
                }
147
                return $value;
148 4
            case 'datetime':
149
                if (is_string($value)) {
150
                    $temp = strtotime($value);
151
                    if (!$temp) {
152
                        return null;
153
                    }
154
                    return date('Y-m-d H:i:s', strtotime($value));
155
                }
156
                if (is_int($value)) {
157
                    return date('Y-m-d H:i:s', $value);
158
                }
159
                if ($value instanceof \DateTime) {
160
                    return $value->format('Y-m-d H:i:s');
161
                }
162
                return $value;
163 4
            case 'enum':
164
                if (is_int($value)) {
165
                    return $value;
166
                }
167
                if (!in_array($value, $col->getValues())) {
168
                    return 0;
169
                }
170
                return $value;
171 4
            case 'int':
172 1
                return (int)$value;
173
            default:
174 3
                return $value;
175
        }
176
    }
177
    /**
178
     * Filter the results by a column and a value
179
     * @param  string $column  the column name to filter by (related columns can be used - for example: author.name)
180
     * @param  mixed  $value   a required value, array of values or range of values (range example: ['beg'=>1,'end'=>3])
181
     * @param  bool   $negate  optional boolean indicating that the filter should be negated
182
     * @return $this
183
     */
184 2
    public function filter(string $column, $value, bool $negate = false) : TableQuery
185
    {
186 2
        list($name, $column) = array_values($this->getColumn($column));
187 2
        if (is_null($value)) {
188
            return $negate ?
189
                $this->where($name . ' IS NOT NULL') :
190
                $this->where($name . ' IS NULL');
191
        }
192 2
        if (!is_array($value)) {
193 2
            return $negate ?
194
                $this->where(
195
                    $name . ' <> ?',
196
                    [ $this->normalizeValue($column, $value) ]
197
                ) :
198 2
                $this->where(
199 2
                    $name . ' = ?',
200 2
                    [ $this->normalizeValue($column, $value) ]
201
                );
202
        }
203
        if (isset($value['beg']) && isset($value['end'])) {
204
            return $negate ?
205
                $this->where(
206
                    $name.' NOT BETWEEN ? AND ?',
207
                    [
208
                        $this->normalizeValue($column, $value['beg']),
209
                        $this->normalizeValue($column, $value['end'])
210
                    ]
211
                ) :
212
                $this->where(
213
                    $name.' BETWEEN ? AND ?',
214
                    [
215
                        $this->normalizeValue($column, $value['beg']),
216
                        $this->normalizeValue($column, $value['end'])
217
                    ]
218
                );
219
        }
220
        return $negate ?
221
            $this->where(
222
                $name . ' NOT IN (??)',
223
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
224
            ) :
225
            $this->where(
226
                $name . ' IN (??)',
227
                [ array_map(function ($v) use ($column) { return $this->normalizeValue($column, $v); }, $value) ]
228
            );
229
    }
230
    /**
231
     * Sort by a column
232
     * @param  string       $column the column name to sort by (related columns can be used - for example: author.name)
233
     * @param  bool|boolean $desc   should the sorting be in descending order, defaults to `false`
234
     * @return $this
235
     */
236
    public function sort(string $column, bool $desc = false) : TableQuery
237
    {
238
        return $this->order($this->getColumn($column)['name'] . ' ' . ($desc ? 'DESC' : 'ASC'));
239
    }
240
    /**
241
     * Group by a column (or columns)
242
     * @param  string|array        $column the column name (or names) to group by
243
     * @return $this
244
     */
245 1
    public function group($column) : TableQuery
246
    {
247 1
        if (!is_array($column)) {
248 1
            $column = [ $column ];
249
        }
250 1
        foreach ($column as $k => $v) {
251 1
            $column[$k] = $this->getColumn($v)['name'];
252
        }
253 1
        return $this->groupBy(implode(', ', $column), []);
254
    }
255
    /**
256
     * Get a part of the data
257
     * @param  int|integer $page    the page number to get (1-based), defaults to 1
258
     * @param  int|integer $perPage the number of records per page - defaults to 25
259
     * @return $this
260
     */
261
    public function paginate(int $page = 1, int $perPage = 25) : TableQuery
262
    {
263
        return $this->limit($perPage, ($page - 1) * $perPage);
264
    }
265 1
    public function __call($name, $data)
266
    {
267 1
        if (strpos($name, 'filterBy') === 0) {
268
            return $this->filter(strtolower(substr($name, 8)), $data[0]);
269
        }
270 1
        if (strpos($name, 'sortBy') === 0) {
271
            return $this->sort(strtolower(substr($name, 6)), $data[0]);
272
        }
273 1
        if (strpos($name, 'groupBy') === 0) {
274 1
            return $this->group(strtolower(substr($name, 7)));
275
        }
276
    }
277
    /**
278
     * Remove all filters, sorting, etc
279
     * @return $this
280
     */
281
    public function reset() : TableQuery
282
    {
283
        $this->where = [];
284
        $this->joins = [];
285
        $this->group = [];
286
        $this->withr = [];
287
        $this->order = [];
288
        $this->having = [];
289
        $this->li_of = [0,0];
290
        $this->qiterator = null;
291
        return $this;
292
    }
293
    /**
294
     * Apply advanced grouping
295
     * @param  string $sql    SQL statement to use in the GROUP BY clause
296
     * @param  array  $params optional params for the statement (defaults to an empty array)
297
     * @return $this
298
     */
299 1
    public function groupBy(string $sql, array $params = []) : TableQuery
300
    {
301 1
        $this->qiterator = null;
302 1
        $this->group = [ $sql, $params ];
303 1
        return $this;
304
    }
305
    /**
306
     * Join a table to the query (no need to do this for relations defined with foreign keys)
307
     * @param  Table|string $table     the table to join
308
     * @param  array        $fields    what to join on (joined_table_field => other_field) 
309
     * @param  string|null  $name      alias for the join, defaults to the table name 
310
     * @param  bool         $multiple  are multiple rows joined (results in a LEFT JOIN), default to true 
311
     * @return $this
312
     */
313 1
    public function join($table, array $fields, string $name = null, bool $multiple = true)
314
    {
315 1
        $table = $table instanceof Table ? $table : $this->db->definition((string)$table);
316 1
        $name = $name ?? $table->getName();
317 1
        if (isset($this->joins[$name]) || $this->definition->hasRelation($name)) {
318
            throw new DBException('Alias / table name already in use');
319
        }
320 1
        $this->joins[$name] = new TableRelation($name, $table, [], $multiple);
321 1
        foreach ($fields as $k => $v) {
322 1
            $k = explode('.', $k, 2);
323 1
            $k = count($k) == 2 ? $k[1] : $k[0];
324 1
            $this->joins[$name]->keymap[$this->getColumn($name . '.' . $k)['name']] = $this->getColumn($v)['name'];
325
        }
326 1
        return $this;
327
    }
328
    /**
329
     * Apply an advanced filter (can be called multiple times)
330
     * @param  string $sql    SQL statement to be used in the where clause
331
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
332
     * @return $this
333
     */
334 3
    public function where(string $sql, array $params = []) : TableQuery
335
    {
336 3
        $this->qiterator = null;
337 3
        $this->where[] = [ $sql, $params ];
338 3
        return $this;
339
    }
340
    /**
341
     * Apply an advanced HAVING filter (can be called multiple times)
342
     * @param  string $sql    SQL statement to be used in the HAING clause
343
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
344
     * @return $this
345
     */
346 1
    public function having(string $sql, array $params = []) : TableQuery
347
    {
348 1
        $this->qiterator = null;
349 1
        $this->having[] = [ $sql, $params ];
350 1
        return $this;
351
    }
352
    /**
353
     * Apply advanced sorting
354
     * @param  string $sql    SQL statement to use in the ORDER clause
355
     * @param  array  $params optional params for the statement (defaults to an empty array)
356
     * @return $this
357
     */
358 1
    public function order(string $sql, array $params = []) : TableQuery
359
    {
360 1
        $this->qiterator = null;
361 1
        $this->order = [ $sql, $params ];
362 1
        return $this;
363
    }
364
    /**
365
     * Apply an advanced limit
366
     * @param  int         $limit  number of rows to return
367
     * @param  int         $offset number of rows to skip from the beginning (defaults to 0)
368
     * @return $this
369
     */
370 1
    public function limit(int $limit, int $offset = 0) : TableQuery
371
    {
372 1
        $this->qiterator = null;
373 1
        $this->li_of = [ $limit, $offset ];
374 1
        return $this;
375
    }
376
    /**
377
     * Get the number of records
378
     * @return int the total number of records (does not respect pagination)
379
     */
380 3
    public function count() : int
381
    {
382 3
        $table = $this->definition->getName();
383 3
        $primary = $this->definition->getPrimaryKey();
384 3
        $sql = 'SELECT COUNT(DISTINCT '.$table.'.'.implode(', '.$table.'.', $primary).') FROM '.$table.' ';
385 3
        $par = [];
386
        
387 3
        $relations = $this->withr;
388 3
        foreach ($this->definition->getRelations() as $k => $v) {
389 3
            foreach ($this->where as $vv) {
390 1
                if (strpos($vv[0], $k . '.') !== false) {
391 1
                    $relations[$k] = [ $v, $table ];
392
                }
393
            }
394 3
            if (isset($this->order[0]) && strpos($this->order[0], $k . '.') !== false) {
395 3
                $relations[$k] = [ $v, $table ];
396
            }
397
        }
398
399 3
        foreach ($this->joins as $k => $v) {
400
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
401
            $tmp = [];
402
            foreach ($v->keymap as $kk => $vv) {
403
                $tmp[] = $kk.' = '.$vv;
404
            }
405
            $sql .= implode(' AND ', $tmp) . ' ';
406
        }
407 3
        foreach ($relations as $k => $v) {
408 1
            $table = $v[1];
409 1
            $v = $v[0];
410 1
            if ($v->pivot) {
411 1
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$k.'_pivot ON ';
412 1
                $tmp = [];
413 1
                foreach ($v->keymap as $kk => $vv) {
414 1
                    $tmp[] = $table.'.'.$kk.' = '.$k.'_pivot.'.$vv.' ';
415
                }
416 1
                $sql .= implode(' AND ', $tmp) . ' ';
417 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$k.' ON ';
418 1
                $tmp = [];
419 1
                foreach ($v->pivot_keymap as $kk => $vv) {
420 1
                    $tmp[] = $k.'.'.$vv.' = '.$k.'_pivot.'.$kk.' ';
421
                }
422 1
                $sql .= implode(' AND ', $tmp) . ' ';
423
            } else {
424 1
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$k.' ON ';
425 1
                $tmp = [];
426 1
                foreach ($v->keymap as $kk => $vv) {
427 1
                    $tmp[] = $table.'.'.$kk.' = '.$k.'.'.$vv.' ';
428
                }
429 1
                if ($v->sql) {
430
                    $tmp[] = $v->sql . ' ';
431
                    $par = array_merge($par, $v->par ?? []);
432
                }
433 1
                $sql .= implode(' AND ', $tmp) . ' ';
434
            }
435
        }
436 3
        if (count($this->where)) {
437 1
            $sql .= 'WHERE ';
438 1
            $tmp = [];
439 1
            foreach ($this->where as $v) {
440 1
                $tmp[] = '(' . $v[0] . ')';
441 1
                $par = array_merge($par, $v[1]);
442
            }
443 1
            $sql .= implode(' AND ', $tmp).' ';
444
        }
445 3
        if (count($this->group)) {
446
            $sql .= 'GROUP BY ' . $this->group[0] . ' ';
447
            $par = array_merge($par, $this->group[1]);
448
        }
449 3
        if (count($this->having)) {
450
            $sql .= 'HAVING ';
451
            $tmp = [];
452
            foreach ($this->having as $v) {
453
                $tmp[] = '(' . $v[0] . ')';
454
                $par = array_merge($par, $v[1]);
455
            }
456
            $sql .= implode(' AND ', $tmp).' ';
457
        }
458 3
        return $this->db->one($sql, $par);
459
    }
460
    /**
461
     * Specify which columns to fetch (be default all table columns are fetched)
462
     * @param  array $fields optional array of columns to select (related columns can be used too)
463
     * @return $this
464
     */
465 13
    public function columns(array $fields) : TableQuery
466
    {
467 13
        foreach ($fields as $k => $v) {
468 13
            if (strpos($v, '*') !== false) {
469
                $temp = explode('.', $v);
470
                if (count($temp) === 1) {
471
                    $table = $this->definition->getName();
472
                    $cols = $this->definition->getColumns();
473
                } else if (count($temp) === 2) {
474
                    $table = $temp[0];
475
                    if ($this->definition->hasRelation($table)) {
476
                        $cols = $this->definition->getRelation($table)->table->getColumns();
477
                    } else if (isset($this->joins[$table])) {
478
                        $cols = $this->joins[$table]->table->getColumns();
479
                    } else {
480
                        throw new DBException('Invalid foreign table name');
481
                    }
482
                } else {
483
                    array_pop($temp);
484
                    $this->with(implode('.', $temp));
485
                    $table = array_reduce(
486
                        $temp,
487
                        function ($carry, $item) use (&$table) {
488
                            return $table->getRelation($item)->table;
489
                        }
490
                    );
491
                    $cols = $table->getColumns();
492
                    $table = implode(static::SEP, $temp);
493
                }
494
                unset($fields[$k]);
495
                foreach ($cols as $col) {
496 13
                    $fields[] = $table . '.' . $col;
497
                }
498
            }
499
        }
500 13
        $primary = $this->definition->getPrimaryKey();
501 13
        foreach ($fields as $k => $v) {
502
            try {
503 13
                $fields[$k] = $this->getColumn($v)['name'];
504 1
            } catch (DBException $e) {
505 13
                $fields[$k] = $v;
506
            }
507
        }
508 13
        foreach ($primary as $field) {
509 13
            $field = $this->getColumn($field)['name'];
510 13
            if (!in_array($field, $fields)) {
511 13
                $fields[] = $field;
512
            }
513
        }
514 13
        $this->fields = $fields;
515 13
        return $this;
516
    }
517
    /**
518
     * Perform the actual fetch
519
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
520
     * @return TableQueryIterator               the query result as an iterator
521
     */
522 11
    public function iterator(array $fields = null) : TableQueryIterator
523
    {
524 11
        if ($this->qiterator) {
525 4
            return $this->qiterator;
526
        }
527 11
        $table = $this->definition->getName();
528 11
        $primary = $this->definition->getPrimaryKey();
529 11
        if ($fields !== null) {
530 1
            $this->columns($fields);
531
        }
532 11
        $relations = $this->withr;
533 11
        foreach ($this->definition->getRelations() as $k => $relation) {
534 11
            foreach ($this->fields as $field) {
535 11
                if (strpos($field, $k . '.') === 0) {
536 11
                    $relations[$k] = [ $relation, $table ];
537
                }
538
            }
539 11
            foreach ($this->where as $v) {
540
                if (strpos($v[0], $k . '.') !== false) {
541
                    $relations[$k] = [ $relation, $table ];
542
                }
543
            }
544 11
            if (isset($this->order[0]) && strpos($this->order[0], $k . '.') !== false) {
545 11
                $relations[$k] = [ $relation, $table ];
546
            }
547
        }
548 11
        $select = [];
549 11
        foreach ($this->fields as $k => $field) {
550 11
            $select[] = $field . (!is_numeric($k) ? ' ' . $k : '');
551
        }
552 11
        foreach ($this->withr as $name => $relation) {
553 4
            foreach ($relation[0]->table->getColumns() as $column) {
554 4
                $select[] = $name . '.' . $column . ' ' . $name . static::SEP . $column;
555
            }
556
        }
557 11
        $sql = 'SELECT '.implode(', ', $select).' FROM '.$table.' ';
558 11
        $par = [];
559 11
        foreach ($this->joins as $k => $v) {
560 1
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
561 1
            $tmp = [];
562 1
            foreach ($v->keymap as $kk => $vv) {
563 1
                $tmp[] = $kk.' = '.$vv;
564
            }
565 1
            $sql .= implode(' AND ', $tmp) . ' ';
566
        }
567 11
        foreach ($relations as $relation => $v) {
568 4
            $table = $v[1];
569 4
            $v = $v[0];
570 4
            if ($v->pivot) {
571 3
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$relation.'_pivot ON ';
572 3
                $tmp = [];
573 3
                foreach ($v->keymap as $kk => $vv) {
574 3
                    $tmp[] = $table.'.'.$kk.' = '.$relation.'_pivot.'.$vv.' ';
575
                }
576 3
                $sql .= implode(' AND ', $tmp) . ' ';
577 3
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$relation.' ON ';
578 3
                $tmp = [];
579 3
                foreach ($v->pivot_keymap as $kk => $vv) {
580 3
                    $tmp[] = $relation.'.'.$vv.' = '.$relation.'_pivot.'.$kk.' ';
581
                }
582 3
                $sql .= implode(' AND ', $tmp) . ' ';
583
            } else {
584 4
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$relation.' ON ';
585 4
                $tmp = [];
586 4
                foreach ($v->keymap as $kk => $vv) {
587 4
                    $tmp[] = $table.'.'.$kk.' = '.$relation.'.'.$vv.' ';
588
                }
589 4
                if ($v->sql) {
590
                    $tmp[] = $v->sql . ' ';
591
                    $par = array_merge($par, $v->par ?? []);
592
                }
593 4
                $sql .= implode(' AND ', $tmp) . ' ';
594
            }
595
        }
596 11
        if (count($this->where)) {
597
            $sql .= 'WHERE ';
598
            $tmp = [];
599
            foreach ($this->where as $v) {
600
                $tmp[] = '(' . $v[0] . ')';
601
                $par = array_merge($par, $v[1]);
602
            }
603
            $sql .= implode(' AND ', $tmp).' ';
604
        }
605 11
        if (count($this->group)) {
606 1
            $sql .= 'GROUP BY ' . $this->group[0] . ' ';
607 1
            $par = array_merge($par, $this->group[1]);
608
        }
609 11
        if (count($this->having)) {
610 1
            $sql .= 'HAVING ';
611 1
            $tmp = [];
612 1
            foreach ($this->having as $v) {
613 1
                $tmp[] = '(' . $v[0] . ')';
614 1
                $par = array_merge($par, $v[1]);
615
            }
616 1
            $sql .= implode(' AND ', $tmp).' ';
617
        }
618 11
        if (count($this->order)) {
619 1
            $sql .= 'ORDER BY ' . $this->order[0] . ' ';
620 1
            $par = array_merge($par, $this->order[1]);
621
        }
622 11
        $porder = [];
623 11
        foreach ($primary as $field) {
624 11
            $porder[] = $this->getColumn($field)['name'];
625
        }
626 11
        $sql .= (count($this->order) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
627
628 11
        if ($this->li_of[0]) {
629 1
            if ($this->db->driverName() === 'oracle') {
630
                if ((int)$this->db->driverOption('version', 0) >= 12) {
631
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
632
                } else {
633
                    $f = array_map(function ($v) {
634
                        $v = explode(' ', trim($v), 2);
635
                        if (count($v) === 2) { return $v[1]; }
636
                        $v = explode('.', $v[0], 2);
637
                        return count($v) === 2 ? $v[1] : $v[0];
638
                    }, $select);
639
                    $sql = "SELECT " . implode(', ', $f) . " 
640
                            FROM (
641
                                SELECT tbl__.*, rownum rnum__ FROM (
642
                                    " . $sql . "
643
                                ) tbl__ 
644
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
645
                            ) WHERE rnum__ > " . $this->li_of[1];
646
                }
647
            } else {
648 1
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
649
            }
650
        }
651 11
        return $this->qiterator = new TableQueryIterator(
652 11
            $this->db->get($sql, $par), 
653 11
            $this->definition->getPrimaryKey(),
654 11
            $this->withr
655
        );
656
    }
657
    /**
658
     * Perform the actual fetch
659
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
660
     * @return array               the query result as an array
661
     */
662 1
    public function select(array $fields = null) : array
663
    {
664 1
        return iterator_to_array($this->iterator($fields));
665
    }
666
    /**
667
     * Insert a new row in the table
668
     * @param  array   $data   key value pairs, where each key is the column name and the value is the value to insert
669
     * @return array           the inserted ID where keys are column names and values are column values
670
     */
671 1
    public function insert(array $data) : array
672
    {
673 1
        $table = $this->definition->getName();
674 1
        $columns = $this->definition->getFullColumns();
675 1
        $insert = [];
676 1
        foreach ($data as $column => $value) {
677 1
            if (isset($columns[$column])) {
678 1
                $insert[$column] = $this->normalizeValue($columns[$column], $value);
679
            }
680
        }
681 1
        if (!count($insert)) {
682
            throw new DBException('No valid columns to insert');
683
        }
684 1
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', array_keys($insert)).') VALUES (??)';
685 1
        $par = [$insert];
686 1
        if ($this->db->driverName() === 'oracle') {
687
            $primary = $this->definition->getPrimaryKey();
688
            $ret = [];
689
            foreach ($primary as $k) {
690
                $ret[$k] = str_repeat(' ', 255);
691
                $par[] = &$ret[$k];
692
            }
693
            $sql .= ' RETURNING ' . implode(',', $primary) . ' INTO ' . implode(',', array_fill(0, count($primary), '?'));
694
            $this->db->query($sql, $par);
695
            return $ret;
696
        } else {
697 1
            $ret = [];
698 1
            $ins = $this->db->query($sql, $par)->insertID();
699 1
            foreach ($this->definition->getPrimaryKey() as $k) {
700 1
                $ret[$k] = $data[$k] ?? $ins;
701
            }
702 1
            return $ret;
703
        }
704
    }
705
    /**
706
     * Update the filtered rows with new data
707
     * @param  array  $data key value pairs, where each key is the column name and the value is the value to insert
708
     * @return int          the number of affected rows
709
     */
710 1
    public function update(array $data) : int
711
    {
712 1
        $table = $this->definition->getName();
713 1
        $columns = $this->definition->getFullColumns();
714 1
        $update = [];
715 1
        foreach ($data as $column => $value) {
716 1
            if (isset($columns[$column])) {
717 1
                $update[$column] = $this->normalizeValue($columns[$column], $value);
718
            }
719
        }
720 1
        if (!count($update)) {
721
            throw new DBException('No valid columns to update');
722
        }
723 1
        $sql = 'UPDATE '.$table.' SET ';
724 1
        $par = [];
725
        $sql .= implode(', ', array_map(function ($v) { return $v . ' = ?'; }, array_keys($update))) . ' ';
726 1
        $par = array_merge($par, array_values($update));
727 1
        if (count($this->where)) {
728 1
            $sql .= 'WHERE ';
729 1
            $tmp = [];
730 1
            foreach ($this->where as $v) {
731 1
                $tmp[] = $v[0];
732 1
                $par = array_merge($par, $v[1]);
733
            }
734 1
            $sql .= implode(' AND ', $tmp) . ' ';
735
        }
736 1
        if (count($this->order)) {
737
            $sql .= $this->order[0];
738
            $par = array_merge($par, $this->order[1]);
739
        }
740 1
        return $this->db->query($sql, $par)->affected();
741
    }
742
    /**
743
     * Delete the filtered rows from the DB
744
     * @return int the number of deleted rows
745
     */
746 1
    public function delete() : int
747
    {
748 1
        $table = $this->definition->getName();
749 1
        $sql = 'DELETE FROM '.$table.' ';
750 1
        $par = [];
751 1
        if (count($this->where)) {
752 1
            $sql .= 'WHERE ';
753 1
            $tmp = [];
754 1
            foreach ($this->where as $v) {
755 1
                $tmp[] = $v[0];
756 1
                $par = array_merge($par, $v[1]);
757
            }
758 1
            $sql .= implode(' AND ', $tmp) . ' ';
759
        }
760 1
        if (count($this->order)) {
761
            $sql .= $this->order[0];
762
            $par = array_merge($par, $this->order[1]);
763
        }
764 1
        return $this->db->query($sql, $par)->affected();
765
    }
766
    /**
767
     * Solve the n+1 queries problem by prefetching a relation by name
768
     * @param  string $relation the relation name to fetch along with the data
769
     * @return $this
770
     */
771 4
    public function with(string $relation) : TableQuery
772
    {
773 4
        $this->qiterator = null;
774 4
        $parts = explode('.', $relation);
775 4
        $table = $this->definition;
776 4
        array_reduce(
777 4
            $parts,
778 4
            function ($carry, $item) use (&$table) {
779 4
                $relation = $table->getRelation($item);
780 4
                if (!$relation) {
781
                    throw new DBException('Invalid relation name');
782
                }
783 4
                $name = $carry ? $carry . static::SEP . $item : $item;
784 4
                $this->withr[$name] = [ $relation, $carry ?? $table->getName() ];
785 4
                $table = $relation->table;
786 4
                return $name;
787 4
            }
788
        );
789 4
        return $this;
790
    }
791
792 1
    public function getIterator()
793
    {
794 1
        return $this->iterator();
795
    }
796
797 9
    public function offsetGet($offset)
798
    {
799 9
        return $this->iterator()->offsetGet($offset);
800
    }
801
    public function offsetExists($offset)
802
    {
803
        return $this->iterator()->offsetExists($offset);
804
    }
805
    public function offsetUnset($offset)
806
    {
807
        return $this->iterator()->offsetUnset($offset);
808
    }
809
    public function offsetSet($offset, $value)
810
    {
811
        return $this->iterator()->offsetSet($offset, $value);
812
    }
813
814
    public function collection(array $fields = null) : Collection
815
    {
816
        return new Collection($this->iterator($fields));
817
    }
818
}
819