Completed
Push — master ( fe07e3...4a77f9 )
by Ivan
03:27
created

TableQuery::__call()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.8437

Importance

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