Completed
Push — master ( 36726b...e4d56c )
by Ivan
09:54 queued 08:29
created

TableQuery::sort()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
cc 2
nc 1
nop 2
crap 6
1
<?php
2
namespace vakata\database\schema;
3
4
use vakata\collection\Collection;
5
use vakata\database\DBInterface;
6
use vakata\database\DBException;
7
use vakata\database\ResultInterface;
8
9
/**
10
 * A database query class
11
 */
12
class TableQuery implements \IteratorAggregate, \ArrayAccess, \Countable
13
{
14
    const SEP = '___';
15
    /**
16
     * @var DBInterface
17
     */
18
    protected $db;
19
    /**
20
     * @var Table
21
     */
22
    protected $definition;
23
    /**
24
     * @var TableQueryIterator|null
25
     */
26
    protected $qiterator;
27
28
    /**
29
     * @var array
30
     */
31
    protected $where = [];
32
    /**
33
     * @var array
34
     */
35
    protected $order = [];
36
    /**
37
     * @var array
38
     */
39
    protected $group = [];
40
    /**
41
     * @var array
42
     */
43
    protected $having = [];
44
    /**
45
     * @var int[]
46
     */
47
    protected $li_of = [0,0,0];
48
    /**
49
     * @var array
50
     */
51
    protected $fields = [];
52
    /**
53
     * @var array
54
     */
55
    protected $withr = [];
56
    /**
57
     * @var array
58
     */
59
    protected $joins = [];
60
    /**
61
     * @var array
62
     */
63
    protected $pkey = [];
64
    /**
65
     * @var array
66
     */
67
    protected $aliases = [];
68
69
    /**
70
     * Create an instance
71
     * @param  DBInterface    $db         the database connection
72
     * @param  Table|string   $table      the name or definition of the main table in the query
73
     */
74 156
    public function __construct(DBInterface $db, $table)
75
    {
76 156
        $this->db = $db;
77 156
        $this->definition = $table instanceof Table ? $table : $this->db->definition((string)$table);
78 156
        $primary = $this->definition->getPrimaryKey();
79 156
        $columns = $this->definition->getColumns();
80 156
        $this->pkey = count($primary) ? $primary : $columns;
81 156
        $this->columns($columns);
82 156
    }
83
    public function __clone()
84
    {
85
        $this->reset();
86
    }
87
    /**
88
     * Get the table definition of the queried table
89
     * @return Table        the definition
90
     */
91
    public function getDefinition() : Table
92
    {
93
        return $this->definition;
94
    }
95
96 156
    protected function getColumn($column)
97
    {
98 156
        $column = explode('.', $column);
99 156
        if (count($column) === 1) {
100 156
            $column = [ $this->definition->getName(), $column[0] ];
101 156
            $col = $this->definition->getColumn($column[1]);
102 156
            if (!$col) {
103 156
                throw new DBException('Invalid column name in own table');
104
            }
105 28
        } elseif (count($column) === 2) {
106 28
            if ($column[0] === $this->definition->getName()) {
107
                $col = $this->definition->getColumn($column[1]);
108
                if (!$col) {
109
                    throw new DBException('Invalid column name in own table');
110
                }
111
            } else {
112 28
                if ($this->definition->hasRelation($column[0])) {
113 24
                    $col = $this->definition->getRelation($column[0])->table->getColumn($column[1]);
114 24
                    if (!$col) {
115 24
                        throw new DBException('Invalid column name in related table');
116
                    }
117 4
                } elseif (isset($this->joins[$column[0]])) {
118 4
                    $col = $this->joins[$column[0]]->table->getColumn($column[1]);
119 4
                    if (!$col) {
120 4
                        throw new DBException('Invalid column name in related table');
121
                    }
122
                } else {
123 28
                    throw new DBException('Invalid foreign table name: ' . implode(',', $column));
124
                }
125
            }
126
        } else {
127
            $name = array_pop($column);
128
            $this->with(implode('.', $column));
129
            $table = $this->definition;
130
            $table = array_reduce(
131
                $column,
132
                function ($carry, $item) use (&$table) {
133
                    $table = $table->getRelation($item)->table;
134
                    return $table;
135
                }
136
            );
137
            $col = $table->getColumn($name);
138
            $column = [ implode(static::SEP, $column), $name ];
139
        }
140 156
        return [ 'name' => implode('.', $column), 'data' => $col ];
141
    }
142 76
    protected function normalizeValue(TableColumn $col, $value)
143
    {
144 76
        $strict = (int)$this->db->driverOption('strict', 0) > 0;
145 76
        if ($value === null && $col->isNullable()) {
146
            return null;
147
        }
148 76
        switch ($col->getBasicType()) {
149 76
            case 'date':
150
                if (is_string($value)) {
151
                    $temp = strtotime($value);
152
                    if (!$temp) {
153
                        if ($strict) {
154
                            throw new DBException('Invalid value for date column ' . $col->getName());
155
                        }
156
                        return null;
157
                    }
158
                    return date('Y-m-d', $temp);
159
                }
160
                if (is_int($value)) {
161
                    return date('Y-m-d', $value);
162
                }
163
                if ($value instanceof \DateTime) {
164
                    return $value->format('Y-m-d');
165
                }
166
                if ($strict) {
167
                    throw new DBException('Invalid value (unknown data type) for date column ' . $col->getName());
168
                }
169
                return $value;
170 76
            case 'datetime':
171
                if (is_string($value)) {
172
                    $temp = strtotime($value);
173
                    if (!$temp) {
174
                        if ($strict) {
175
                            throw new DBException('Invalid value for datetime column ' . $col->getName());
176
                        }
177
                        return null;
178
                    }
179
                    return date('Y-m-d H:i:s', $temp);
180
                }
181
                if (is_int($value)) {
182
                    return date('Y-m-d H:i:s', $value);
183
                }
184
                if ($value instanceof \DateTime) {
185
                    return $value->format('Y-m-d H:i:s');
186
                }
187
                if ($strict) {
188
                    throw new DBException('Invalid value (unknown data type) for datetime column ' . $col->getName());
189
                }
190
                return $value;
191 76
            case 'enum':
192
                $values = $col->getValues();
193
                if (is_int($value)) {
194
                    if (!isset($values[$value])) {
195
                        if ($strict) {
196
                            throw new DBException('Invalid value (using integer) for enum ' . $col->getName());
197
                        }
198
                        return $value;
199
                    }
200
                    return $values[$value];
201
                }
202
                if (!in_array($value, $col->getValues())) {
203
                    if ($strict) {
204
                        throw new DBException('Invalid value for enum ' . $col->getName());
205
                    }
206
                    return 0;
207
                }
208
                return $value;
209 76
            case 'int':
210 22
                return (int)preg_replace('([^+\-0-9]+)', '', $value);
211 62
            case 'float':
212
                return (float)preg_replace('([^+\-0-9.]+)', '', str_replace(',', '.', $value));
213 62
            case 'text':
214
                // check using strlen first, in order to avoid hitting mb_ functions which might be polyfilled
215
                // because the polyfill is quite slow
216 62
                if ($col->hasLength() && strlen($value) > $col->getLength() && mb_strlen($value) > $col->getLength()) {
217 8
                    if ($strict) {
218 4
                        throw new DBException('Invalid value for text column ' . $col->getName());
219
                    }
220 4
                    return mb_substr($value, 0, $col->getLength());
221
                }
222 56
                return $value;
223
            default: // time, blob, etc
224
                return $value;
225
        }
226
    }
227
228 60
    protected function filterSQL(string $column, $value, bool $negate = false) : array
229
    {
230 60
        list($name, $column) = array_values($this->getColumn($column));
231 60
        if (is_array($value) && count($value) === 1 && isset($value['not'])) {
232 4
            $negate = true;
233 4
            $value = $value['not'];
234
        }
235 60
        if (is_array($value) && count($value) === 1 && isset($value['like'])) {
236 4
            $value = $value['like'];
237
            // str_replace(['%', '_'], ['\\%','\\_'], $q)
238 4
            return $negate ?
239
                [
240
                    $name . ' NOT LIKE ?',
241
                    [ $this->normalizeValue($column, $value) ]
242
                ] :
243
                [
244 4
                    $name . ' LIKE ?',
245 4
                    [ $this->normalizeValue($column, $value) ]
246
                ];
247
        }
248 56
        if (is_null($value)) {
249
            return $negate ?
250
                [ $name . ' IS NOT NULL', [] ]:
251
                [ $name . ' IS NULL', [] ];
252
        }
253 56
        if (!is_array($value)) {
254 56
            return $negate ?
255
                [
256 4
                    $name . ' <> ?',
257 4
                    [ $this->normalizeValue($column, $value) ]
258
                ] :
259
                [
260 56
                    $name . ' = ?',
261 56
                    [ $this->normalizeValue($column, $value) ]
262
                ];
263
        }
264 12
        if (isset($value['beg']) && strlen($value['beg']) && (!isset($value['end']) || !strlen($value['end']))) {
265
            $value = [ 'gte' => $value['beg'] ];
266
        }
267 12
        if (isset($value['end']) && strlen($value['end']) && (!isset($value['beg']) || !strlen($value['beg']))) {
268
            $value = [ 'lte' => $value['end'] ];
269
        }
270 12
        if (isset($value['beg']) && isset($value['end'])) {
271
            return $negate ?
272
                [
273
                    $name.' NOT BETWEEN ? AND ?',
274
                    [
275
                        $this->normalizeValue($column, $value['beg']),
276
                        $this->normalizeValue($column, $value['end'])
277
                    ]
278
                ] :
279
                [
280
                    $name.' BETWEEN ? AND ?',
281
                    [
282
                        $this->normalizeValue($column, $value['beg']),
283
                        $this->normalizeValue($column, $value['end'])
284
                    ]
285
                ];
286
        }
287 12
        if (isset($value['gt']) || isset($value['lt']) || isset($value['gte']) || isset($value['lte'])) {
288 8
            $sql = [];
289 8
            $par = [];
290 8
            if (isset($value['gt'])) {
291 4
                $sql[] = $name. ' ' . ($negate ? '<=' : '>') . ' ?';
292 4
                $par[] = $this->normalizeValue($column, $value['gt']);
293
            }
294 8
            if (isset($value['gte'])) {
295 4
                $sql[] = $name. ' ' . ($negate ? '<' : '>=') . ' ?';
296 4
                $par[] = $this->normalizeValue($column, $value['gte']);
297
            }
298 8
            if (isset($value['lt'])) {
299 8
                $sql[] = $name. ' ' . ($negate ? '>=' : '<') . ' ?';
300 8
                $par[] = $this->normalizeValue($column, $value['lt']);
301
            }
302 8
            if (isset($value['lte'])) {
303 4
                $sql[] = $name. ' ' . ($negate ? '>' : '<=') . ' ?';
304 4
                $par[] = $this->normalizeValue($column, $value['lte']);
305
            }
306
            return [
307 8
                '(' . implode(' AND ', $sql) . ')',
308 8
                $par
309
            ];
310
        }
311 8
        return $negate ?
312
            [
313
                $name . ' NOT IN (??)',
314
                [ array_map(function ($v) use ($column) {
315
                    return $this->normalizeValue($column, $v);
316
                }, $value) ]
317
            ] :
318
            [
319 8
                $name . ' IN (??)',
320 8
                [ array_map(function ($v) use ($column) {
321 8
                    return $this->normalizeValue($column, $v);
322 8
                }, $value) ]
323
            ];
324
    }
325
    /**
326
     * Filter the results by a column and a value
327
     * @param  string $column  the column name to filter by (related columns can be used - for example: author.name)
328
     * @param  mixed  $value   a required value, array of values or range of values (range example: ['beg'=>1,'end'=>3])
329
     * @param  bool   $negate  optional boolean indicating that the filter should be negated
330
     * @return $this
331
     */
332 52
    public function filter(string $column, $value, bool $negate = false) : self
333
    {
334 52
        $sql = $this->filterSQL($column, $value, $negate);
335 52
        return strlen($sql[0]) ? $this->where($sql[0], $sql[1]) : $this;
336
    }
337
    /**
338
     * Filter the results matching any of the criteria
339
     * @param  array $criteria  each row is a column, value and optional negate flag (same as filter method)
340
     * @return $this
341
     */
342 16
    public function any(array $criteria) : self
343
    {
344 16
        $sql = [];
345 16
        $par = [];
346 16
        foreach ($criteria as $row) {
347 16
            if (isset($row[1])) {
348 16
                $temp = $this->filterSQL($row[0], $row[1] ?? null, $row[2] ?? false);
349 16
                $sql[] = $temp[0];
350 16
                $par = array_merge($par, $temp[1]);
351
            }
352
        }
353 16
        return $this->where('(' . implode(' OR ', $sql) . ')', $par);
354
    }
355
    /**
356
     * Filter the results matching all of the criteria
357
     * @param  array $criteria  each row is a column, value and optional negate flag (same as filter method)
358
     * @return $this
359
     */
360 12
    public function all(array $criteria) : self
361
    {
362 12
        $sql = [];
363 12
        $par = [];
364 12
        foreach ($criteria as $row) {
365 12
            if (isset($row[1])) {
366 12
                $temp = $this->filterSQL($row[0], $row[1] ?? null, $row[2] ?? false);
367 12
                $sql[] = $temp[0];
368 12
                $par = array_merge($par, $temp[1]);
369
            }
370
        }
371 12
        return $this->where('(' . implode(' AND ', $sql) . ')', $par);
372
    }
373
    /**
374
     * Sort by a column
375
     * @param  string       $column the column name to sort by (related columns can be used - for example: author.name)
376
     * @param  bool|boolean $desc   should the sorting be in descending order, defaults to `false`
377
     * @return $this
378
     */
379
    public function sort(string $column, bool $desc = false) : self
380
    {
381
        return $this->order($this->getColumn($column)['name'] . ' ' . ($desc ? 'DESC' : 'ASC'));
382
    }
383
    /**
384
     * Group by a column (or columns)
385
     * @param  string|array        $column the column name (or names) to group by
386
     * @return $this
387
     */
388 4
    public function group($column) : self
389
    {
390 4
        if (!is_array($column)) {
391 4
            $column = [ $column ];
392
        }
393 4
        foreach ($column as $k => $v) {
394 4
            $column[$k] = $this->getColumn($v)['name'];
395
        }
396 4
        return $this->groupBy(implode(', ', $column), []);
397
    }
398
    /**
399
     * Get a part of the data
400
     * @param  int|integer $page    the page number to get (1-based), defaults to 1
401
     * @param  int|integer $perPage the number of records per page - defaults to 25
402
     * @return $this
403
     */
404
    public function paginate(int $page = 1, int $perPage = 25) : self
405
    {
406
        return $this->limit($perPage, ($page - 1) * $perPage);
407
    }
408 4
    public function __call($name, $data)
409
    {
410 4
        if (strpos($name, 'filterBy') === 0) {
411
            return $this->filter(strtolower(substr($name, 8)), $data[0]);
412
        }
413 4
        if (strpos($name, 'sortBy') === 0) {
414
            return $this->sort(strtolower(substr($name, 6)), $data[0]);
415
        }
416 4
        if (strpos($name, 'groupBy') === 0) {
417 4
            return $this->group(strtolower(substr($name, 7)));
418
        }
419
    }
420
    /**
421
     * Remove all filters, sorting, etc
422
     * @return $this
423
     */
424 12
    public function reset() : self
425
    {
426 12
        $this->where = [];
427 12
        $this->joins = [];
428 12
        $this->group = [];
429 12
        $this->withr = [];
430 12
        $this->order = [];
431 12
        $this->having = [];
432 12
        $this->aliases = [];
433 12
        $this->li_of = [0,0,0];
434 12
        $this->qiterator = null;
435 12
        return $this;
436
    }
437
    /**
438
     * Apply advanced grouping
439
     * @param  string $sql    SQL statement to use in the GROUP BY clause
440
     * @param  array  $params optional params for the statement (defaults to an empty array)
441
     * @return $this
442
     */
443 4
    public function groupBy(string $sql, array $params = []) : self
444
    {
445 4
        $this->qiterator = null;
446 4
        $this->group = [ $sql, $params ];
447 4
        return $this;
448
    }
449
    /**
450
     * Join a table to the query (no need to do this for relations defined with foreign keys)
451
     * @param  Table|string $table     the table to join
452
     * @param  array        $fields    what to join on (joined_table_field => other_field)
453
     * @param  string|null  $name      alias for the join, defaults to the table name
454
     * @param  bool         $multiple  are multiple rows joined (results in a LEFT JOIN), default to true
455
     * @return $this
456
     */
457 4
    public function join($table, array $fields, string $name = null, bool $multiple = true)
458
    {
459 4
        $table = $table instanceof Table ? $table : $this->db->definition((string)$table);
460 4
        $name = $name ?? $table->getName();
461 4
        if (isset($this->joins[$name]) || $this->definition->hasRelation($name)) {
462
            throw new DBException('Alias / table name already in use');
463
        }
464 4
        $this->joins[$name] = new TableRelation($name, $table, [], $multiple);
465 4
        foreach ($fields as $k => $v) {
466 4
            $k = explode('.', $k, 2);
467 4
            $k = count($k) == 2 ? $k[1] : $k[0];
468 4
            $this->joins[$name]->keymap[$this->getColumn($name . '.' . $k)['name']] = $this->getColumn($v)['name'];
469
        }
470 4
        return $this;
471
    }
472
    /**
473
     * Apply an advanced filter (can be called multiple times)
474
     * @param  string $sql    SQL statement to be used in the where clause
475
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
476
     * @return $this
477
     */
478 64
    public function where(string $sql, array $params = []) : self
479
    {
480 64
        $this->qiterator = null;
481 64
        $this->where[] = [ $sql, $params ];
482 64
        return $this;
483
    }
484
    /**
485
     * Apply an advanced HAVING filter (can be called multiple times)
486
     * @param  string $sql    SQL statement to be used in the HAING clause
487
     * @param  array  $params parameters for the SQL statement (defaults to an empty array)
488
     * @return $this
489
     */
490 4
    public function having(string $sql, array $params = []) : self
491
    {
492 4
        $this->qiterator = null;
493 4
        $this->having[] = [ $sql, $params ];
494 4
        return $this;
495
    }
496
    /**
497
     * Apply advanced sorting
498
     * @param  string $sql    SQL statement to use in the ORDER clause
499
     * @param  array  $params optional params for the statement (defaults to an empty array)
500
     * @return $this
501
     */
502 8
    public function order(string $sql, array $params = []) : self
503
    {
504 8
        $this->qiterator = null;
505 8
        $name = null;
506 8
        if (!count($params)) {
507 8
            $name = preg_replace('(\s+(ASC|DESC)\s*$)i', '', $sql);
508
            try {
509 8
                $name = $this->getColumn(trim($name))['name'];
510 4
            } catch (\Exception $e) {
511 4
                $name = null;
512
            }
513
        }
514 8
        $this->order = [ $sql, $params, $name ];
515 8
        return $this;
516
    }
517
    /**
518
     * Apply an advanced limit
519
     * @param  int         $limit  number of rows to return
520
     * @param  int         $offset number of rows to skip from the beginning (defaults to 0)
521
     * @return $this
522
     */
523 4
    public function limit(int $limit, int $offset = 0, bool $limitOnMainTable = false) : self
524
    {
525 4
        $this->qiterator = null;
526 4
        $this->li_of = [ $limit, $offset, $limitOnMainTable ? 1 : 0 ];
527 4
        return $this;
528
    }
529
    /**
530
     * Get the number of records
531
     * @return int the total number of records (does not respect pagination)
532
     */
533 44
    public function count() : int
534
    {
535 44
        $aliases = [];
536 44
        $getAlias = function ($name) use (&$aliases) {
537
            // to bypass use: return $name;
538 12
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
539 44
        };
540 44
        $table = $this->definition->getName();
541 44
        $sql = 'SELECT COUNT(DISTINCT '.$table.'.'.implode(', '.$table.'.', $this->pkey).') FROM '.$table.' ';
542 44
        $par = [];
543
        
544 44
        $relations = $this->withr;
545 44
        foreach ($relations as $k => $v) {
546
            $getAlias($k);
547
        }
548 44
        $w = $this->where;
549 44
        $h = $this->having;
550 44
        $o = $this->order;
551 44
        $g = $this->group;
552 44
        $j = array_map(function ($v) {
553
            return clone $v;
554 44
        }, $this->joins);
555 44
        foreach ($this->definition->getRelations() as $k => $v) {
556 44
            foreach ($w as $kk => $vv) {
557 24
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
558 12
                    $relations[$k] = [ $v, $table ];
559 12
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
560
                }
561
            }
562 44
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
563
                $relations[$k] = [ $v, $table ];
564
            }
565 44
            foreach ($h as $kk => $vv) {
566
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
567
                    $relations[$k] = [ $v, $table ];
568
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
569
                }
570
            }
571 44
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
572
                $relations[$k] = [ $v, $table ];
573
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
574
            }
575 44
            foreach ($j as $kk => $vv) {
576
                foreach ($vv->keymap as $kkk => $vvv) {
577
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vvv)) {
578
                        $relations[$k] = [ $v, $table ];
579
                        $j[$kk]->keymap[$kkk] =
580
                            preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vvv);
581
                    }
582
                }
583
            }
584
        }
585
586 44
        foreach ($j as $k => $v) {
587
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
588
            $tmp = [];
589
            foreach ($v->keymap as $kk => $vv) {
590
                $tmp[] = $kk.' = '.$vv;
591
            }
592
            $sql .= implode(' AND ', $tmp) . ' ';
593
        }
594 44
        foreach ($relations as $k => $v) {
595 12
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
596 12
            $v = $v[0];
597 12
            if ($v->pivot) {
598 12
                $alias = $getAlias($k.'_pivot');
599 12
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
600 12
                $tmp = [];
601 12
                foreach ($v->keymap as $kk => $vv) {
602 12
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
603
                }
604 12
                $sql .= implode(' AND ', $tmp) . ' ';
605 12
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
606 12
                $tmp = [];
607 12
                foreach ($v->pivot_keymap as $kk => $vv) {
608 12
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
609
                }
610 12
                $sql .= implode(' AND ', $tmp) . ' ';
611
            } else {
612 12
                $alias = $getAlias($k);
613 12
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
614 12
                $tmp = [];
615 12
                foreach ($v->keymap as $kk => $vv) {
616 12
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
617
                }
618 12
                if ($v->sql) {
619
                    $tmp[] = $v->sql . ' ';
620
                    $par = array_merge($par, $v->par ?? []);
621
                }
622 12
                $sql .= implode(' AND ', $tmp) . ' ';
623
            }
624
        }
625 44
        if (count($w)) {
626 24
            $sql .= 'WHERE ';
627 24
            $tmp = [];
628 24
            foreach ($w as $v) {
629 24
                $tmp[] = '(' . $v[0] . ')';
630 24
                $par = array_merge($par, $v[1]);
631
            }
632 24
            $sql .= implode(' AND ', $tmp).' ';
633
        }
634 44
        if (count($g)) {
635
            $sql .= 'GROUP BY ' . $g[0] . ' ';
636
            $par = array_merge($par, $g[1]);
637
        }
638 44
        if (count($h)) {
639
            $sql .= 'HAVING ';
640
            $tmp = [];
641
            foreach ($h as $v) {
642
                $tmp[] = '(' . $v[0] . ')';
643
                $par = array_merge($par, $v[1]);
644
            }
645
            $sql .= implode(' AND ', $tmp).' ';
646
        }
647 44
        return $this->db->one($sql, $par);
648
    }
649
    /**
650
     * Specify which columns to fetch (be default all table columns are fetched)
651
     * @param  array $fields optional array of columns to select (related columns can be used too)
652
     * @return $this
653
     */
654 156
    public function columns(array $fields) : self
655
    {
656 156
        foreach ($fields as $k => $v) {
657 156
            if (strpos($v, '*') !== false) {
658
                $temp = explode('.', $v);
659
                if (count($temp) === 1) {
660
                    $table = $this->definition->getName();
661
                    $cols = $this->definition->getColumns();
662
                } elseif (count($temp) === 2) {
663
                    $table = $temp[0];
664
                    if ($this->definition->hasRelation($table)) {
665
                        $cols = $this->definition->getRelation($table)->table->getColumns();
666
                    } elseif (isset($this->joins[$table])) {
667
                        $cols = $this->joins[$table]->table->getColumns();
668
                    } else {
669
                        throw new DBException('Invalid foreign table name');
670
                    }
671
                } else {
672
                    array_pop($temp);
673
                    $this->with(implode('.', $temp));
674
                    $table = array_reduce(
675
                        $temp,
676
                        function ($carry, $item) use (&$table) {
677
                            return $table->getRelation($item)->table;
678
                        }
679
                    );
680
                    $cols = $table->getColumns();
681
                    $table = implode(static::SEP, $temp);
682
                }
683
                unset($fields[$k]);
684
                foreach ($cols as $col) {
685
                    $fields[] = $table . '.' . $col;
686
                }
687
            }
688
        }
689 156
        $primary = $this->definition->getPrimaryKey();
690 156
        foreach ($fields as $k => $v) {
691
            try {
692 156
                $fields[$k] = $this->getColumn($v)['name'];
693 4
            } catch (DBException $e) {
694 4
                $fields[$k] = $v;
695
            }
696
        }
697 156
        foreach ($primary as $field) {
698 156
            $field = $this->getColumn($field)['name'];
699 156
            if (!in_array($field, $fields)) {
700
                $fields[] = $field;
701
            }
702
        }
703 156
        $this->fields = $fields;
704 156
        return $this;
705
    }
706
    /**
707
     * Perform the actual fetch
708
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
709
     * @return mixed               the query result as an iterator (with array access)
710
     */
711 120
    public function iterator(array $fields = null)
712
    {
713 120
        if ($this->qiterator) {
714 80
            return $this->qiterator;
715
        }
716 120
        $aliases = [];
717 120
        $getAlias = function ($name) use (&$aliases) {
718
            // to bypass use: return $name;
719 32
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
720 120
        };
721 120
        $table = $this->definition->getName();
722 120
        if ($fields !== null) {
723 4
            $this->columns($fields);
724
        }
725 120
        $relations = $this->withr;
726 120
        foreach ($relations as $k => $v) {
727 24
            $getAlias($k);
728
        }
729
730 120
        $f = $this->fields;
731 120
        $w = $this->where;
732 120
        $h = $this->having;
733 120
        $o = $this->order;
734 120
        $g = $this->group;
735 120
        $j = array_map(function ($v) {
736 4
            return clone $v;
737 120
        }, $this->joins);
738
739 120
        $porder = [];
740 120
        foreach ($this->definition->getPrimaryKey() as $field) {
741 120
            $porder[] = $this->getColumn($field)['name'];
742
        }
743
744 120
        foreach ($this->definition->getRelations() as $k => $relation) {
745 120
            foreach ($f as $kk => $field) {
746 120
                if (strpos($field, $k . '.') === 0) {
747
                    $relations[$k] = [ $relation, $table ];
748
                    $f[$kk] = str_replace($k . '.', $getAlias($k) . '.', $field);
749
                }
750
            }
751 120
            foreach ($w as $kk => $v) {
752 16
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
753 8
                    $relations[$k] = [ $relation, $table ];
754 8
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
755
                }
756
            }
757 120
            foreach ($h as $kk => $v) {
758 4
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $v[0])) {
759
                    $relations[$k] = [ $relation, $table ];
760
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $v[0]);
761
                }
762
            }
763 120
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
764
                $relations[$k] = [ $relation, $table ];
765
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
766
            }
767 120
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
768
                $relations[$k] = [ $relation, $table ];
769
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
770
            }
771 120
            foreach ($j as $kk => $v) {
772 4
                foreach ($v->keymap as $kkk => $vv) {
773 4
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv)) {
774
                        $relations[$k] = [ $relation, $table ];
775
                        $j[$k]->keymap[$kkk] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv);
776
                    }
777
                }
778
            }
779
        }
780 120
        $select = [];
781 120
        foreach ($f as $k => $field) {
782 120
            $select[] = $field . (!is_numeric($k) ? ' ' . $k : '');
783
        }
784 120
        foreach ($this->withr as $name => $relation) {
785 24
            foreach ($relation[0]->table->getColumns() as $column) {
786 24
                $select[] = $getAlias($name) . '.' . $column . ' ' . $getAlias($name . static::SEP . $column);
787
            }
788
        }
789 120
        $sql = 'SELECT '.implode(', ', $select).' FROM '.$table.' ';
790 120
        $par = [];
791 120
        $many = false;
792 120
        foreach ($j as $k => $v) {
793 4
            if ($v->many) {
794 4
                $many = true;
795
            }
796 4
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
797 4
            $tmp = [];
798 4
            foreach ($v->keymap as $kk => $vv) {
799 4
                $tmp[] = $kk.' = '.$vv;
800
            }
801 4
            $sql .= implode(' AND ', $tmp) . ' ';
802
        }
803 120
        foreach ($relations as $relation => $v) {
804 32
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
805 32
            $v = $v[0];
806 32
            if ($v->many || $v->pivot) {
807 24
                $many = true;
808
            }
809 32
            if ($v->pivot) {
810 20
                $alias = $getAlias($relation.'_pivot');
811 20
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
812 20
                $tmp = [];
813 20
                foreach ($v->keymap as $kk => $vv) {
814 20
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
815
                }
816 20
                $sql .= implode(' AND ', $tmp) . ' ';
817 20
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($relation).' ON ';
818 20
                $tmp = [];
819 20
                foreach ($v->pivot_keymap as $kk => $vv) {
820 20
                    $tmp[] = $getAlias($relation).'.'.$vv.' = '.$alias.'.'.$kk.' ';
821
                }
822 20
                $sql .= implode(' AND ', $tmp) . ' ';
823
            } else {
824 24
                $alias = $getAlias($relation);
825
826 24
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
827 24
                $tmp = [];
828 24
                foreach ($v->keymap as $kk => $vv) {
829 24
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
830
                }
831 24
                if ($v->sql) {
832
                    $tmp[] = $v->sql . ' ';
833
                    $par = array_merge($par, $v->par ?? []);
834
                }
835 24
                $sql .= implode(' AND ', $tmp) . ' ';
836
            }
837
        }
838
839 120
        if ($many && count($porder) && $this->li_of[2] === 1) {
840
            $ids = $this->ids();
841
            if (count($ids)) {
842
                if (count($porder) > 1) {
843
                    $pkw = [];
844
                    foreach ($porder as $name) {
845
                        $pkw[] = $name . ' = ?';
846
                    }
847
                    $pkw = '(' . implode(' AND ', $pkw) . ')';
848
                    $pkp = [];
849
                    foreach ($ids as $id) {
850
                        foreach ($id as $p) {
851
                            $pkp[] = $p;
852
                        }
853
                    }
854
                    $w[] = [
855
                        implode(' OR ', array_fill(0, count($ids), $pkw)),
856
                        $pkp
857
                    ];
858
                } else {
859
                    $w[] = [ $porder[0] . ' IN ('.implode(',', array_fill(0, count($ids), '?')).')', $ids ];
860
                }
861
            } else {
862
                $w[] = [ '1=0', [] ];
863
            }
864
        }
865 120
        if (count($w)) {
866 16
            $sql .= 'WHERE ';
867 16
            $tmp = [];
868 16
            foreach ($w as $v) {
869 16
                $tmp[] = '(' . $v[0] . ')';
870 16
                $par = array_merge($par, $v[1]);
871
            }
872 16
            $sql .= implode(' AND ', $tmp).' ';
873
        }
874 120
        if (count($g)) {
875 4
            $sql .= 'GROUP BY ' . $g[0] . ' ';
876 4
            $par = array_merge($par, $g[1]);
877
        }
878 120
        if (count($h)) {
879 4
            $sql .= 'HAVING ';
880 4
            $tmp = [];
881 4
            foreach ($h as $v) {
882 4
                $tmp[] = '(' . $v[0] . ')';
883 4
                $par = array_merge($par, $v[1]);
884
            }
885 4
            $sql .= implode(' AND ', $tmp).' ';
886
        }
887 120
        if (count($o)) {
888 4
            $sql .= 'ORDER BY ' . $o[0] . ' ';
889 4
            $par = array_merge($par, $o[1]);
890
        }
891 120
        if (count($porder)) {
892 120
            $pdir = (count($o) && strpos($o[0], 'DESC') !== false) ? 'DESC' : 'ASC';
893 120
            $porder = array_map(function ($v) use ($pdir) {
894 120
                return $v . ' ' . $pdir;
895 120
            }, $porder);
896 120
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
897
        }
898 120
        if ((!$many || $this->li_of[2] === 0 || !count($porder)) && $this->li_of[0]) {
899 4
            if ($this->db->driverName() === 'oracle') {
900
                if ((int)$this->db->driverOption('version', 0) >= 12) {
901
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
902
                } else {
903
                    $f = array_map(function ($v) {
904
                        $v = explode(' ', trim($v), 2);
905
                        if (count($v) === 2) {
906
                            return $v[1];
907
                        }
908
                        $v = explode('.', $v[0], 2);
909
                        return count($v) === 2 ? $v[1] : $v[0];
910
                    }, $select);
911
                    $sql = "SELECT " . implode(', ', $f) . " 
912
                            FROM (
913
                                SELECT tbl__.*, rownum rnum__ FROM (
914
                                    " . $sql . "
915
                                ) tbl__ 
916
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
917
                            ) WHERE rnum__ > " . $this->li_of[1];
918
                }
919
            } else {
920 4
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
921
            }
922
        }
923 120
        return $this->qiterator = new TableQueryIterator(
924 120
            $this->db->get($sql, $par, null, false, false),
925 120
            $this->pkey,
926 120
            $this->withr,
927 120
            $aliases
928
        );
929
    }
930
    /**
931
     * Perform the actual fetch
932
     * @param  array|null $fields optional array of columns to select (related columns can be used too)
933
     * @return array               the query result as an array
934
     */
935 4
    public function select(array $fields = null) : array
936
    {
937 4
        return iterator_to_array($this->iterator($fields));
938
    }
939
    /**
940
     * Insert a new row in the table
941
     * @param  array   $data   key value pairs, where each key is the column name and the value is the value to insert
942
     * @return array           the inserted ID where keys are column names and values are column values
943
     */
944 16
    public function insert(array $data) : array
945
    {
946 16
        $table = $this->definition->getName();
947 16
        $columns = $this->definition->getFullColumns();
948 16
        $insert = [];
949 16
        foreach ($data as $column => $value) {
950 16
            if (isset($columns[$column])) {
951 16
                $insert[$column] = $this->normalizeValue($columns[$column], $value);
952
            }
953
        }
954 12
        if (!count($insert)) {
955
            throw new DBException('No valid columns to insert');
956
        }
957 12
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', array_keys($insert)).') VALUES (??)';
958 12
        $par = [$insert];
959 12
        $primary = $this->definition->getPrimaryKey();
960 12
        if (!count($primary)) {
961
            $this->db->query($sql, $par);
962
            return [];
963
        }
964 12
        if ($this->db->driverName() === 'oracle') {
965
            $ret = [];
966
            foreach ($primary as $k) {
967
                $ret[$k] = str_repeat(' ', 255);
968
                $par[] = &$ret[$k];
969
            }
970
            $sql .= ' RETURNING ' . implode(',', $primary) .
971
                ' INTO ' . implode(',', array_fill(0, count($primary), '?'));
972
            $this->db->query($sql, $par);
973
            return $ret;
974
        } else {
975 12
            $ret = [];
976 12
            $ins = $this->db->query($sql, $par)->insertID();
977 12
            foreach ($primary as $k) {
978 12
                $ret[$k] = $data[$k] ?? $ins;
979
            }
980 12
            return $ret;
981
        }
982
    }
983
    /**
984
     * Update the filtered rows with new data
985
     * @param  array  $data key value pairs, where each key is the column name and the value is the value to insert
986
     * @return int          the number of affected rows
987
     */
988 12
    public function update(array $data) : int
989
    {
990 12
        $table = $this->definition->getName();
991 12
        $columns = $this->definition->getFullColumns();
992 12
        $update = [];
993 12
        foreach ($data as $column => $value) {
994 12
            if (isset($columns[$column])) {
995 12
                $update[$column] = $this->normalizeValue($columns[$column], $value);
996
            }
997
        }
998 12
        if (!count($update)) {
999
            throw new DBException('No valid columns to update');
1000
        }
1001 12
        $sql = 'UPDATE '.$table.' SET ';
1002 12
        $par = [];
1003 12
        $sql .= implode(', ', array_map(function ($v) {
1004 12
            return $v . ' = ?';
1005 12
        }, array_keys($update))) . ' ';
1006 12
        $par = array_merge($par, array_values($update));
1007 12
        if (count($this->where)) {
1008 12
            $sql .= 'WHERE ';
1009 12
            $tmp = [];
1010 12
            foreach ($this->where as $v) {
1011 12
                $tmp[] = $v[0];
1012 12
                $par = array_merge($par, $v[1]);
1013
            }
1014 12
            $sql .= implode(' AND ', $tmp) . ' ';
1015
        }
1016 12
        if (count($this->order)) {
1017
            $sql .= $this->order[0];
1018
            $par = array_merge($par, $this->order[1]);
1019
        }
1020 12
        return $this->db->query($sql, $par)->affected();
1021
    }
1022
    /**
1023
     * Delete the filtered rows from the DB
1024
     * @return int the number of deleted rows
1025
     */
1026 8
    public function delete() : int
1027
    {
1028 8
        $table = $this->definition->getName();
1029 8
        $sql = 'DELETE FROM '.$table.' ';
1030 8
        $par = [];
1031 8
        if (count($this->where)) {
1032 8
            $sql .= 'WHERE ';
1033 8
            $tmp = [];
1034 8
            foreach ($this->where as $v) {
1035 8
                $tmp[] = $v[0];
1036 8
                $par = array_merge($par, $v[1]);
1037
            }
1038 8
            $sql .= implode(' AND ', $tmp) . ' ';
1039
        }
1040 8
        if (count($this->order)) {
1041
            $sql .= $this->order[0];
1042
            $par = array_merge($par, $this->order[1]);
1043
        }
1044 8
        return $this->db->query($sql, $par)->affected();
1045
    }
1046
    /**
1047
     * Solve the n+1 queries problem by prefetching a relation by name
1048
     * @param  string $relation the relation name to fetch along with the data
1049
     * @return $this
1050
     */
1051 28
    public function with(string $relation) : self
1052
    {
1053 28
        $this->qiterator = null;
1054 28
        $parts = explode('.', $relation);
1055 28
        $table = $this->definition;
1056 28
        array_reduce(
1057 28
            $parts,
1058 28
            function ($carry, $item) use (&$table) {
1059 28
                $relation = $table->getRelation($item);
1060 28
                if (!$relation) {
1061
                    throw new DBException('Invalid relation name');
1062
                }
1063 28
                $name = $carry ? $carry . static::SEP . $item : $item;
1064 28
                $this->withr[$name] = [ $relation, $carry ?? $table->getName() ];
1065 28
                $table = $relation->table;
1066 28
                return $name;
1067 28
            }
1068
        );
1069 28
        return $this;
1070
    }
1071
1072 20
    public function getIterator()
1073
    {
1074 20
        return $this->iterator();
1075
    }
1076
1077 104
    public function offsetGet($offset)
1078
    {
1079 104
        return $this->iterator()->offsetGet($offset);
1080
    }
1081
    public function offsetExists($offset)
1082
    {
1083
        return $this->iterator()->offsetExists($offset);
1084
    }
1085
    public function offsetUnset($offset)
1086
    {
1087
        $this->iterator()->offsetUnset($offset);
1088
    }
1089
    public function offsetSet($offset, $value)
1090
    {
1091
        $this->iterator()->offsetSet($offset, $value);
1092
    }
1093
1094
    public function collection(array $fields = null) : Collection
1095
    {
1096
        return new Collection($this->iterator($fields));
1097
    }
1098
1099 4
    public function ids()
1100
    {
1101 4
        if (count($this->group)) {
1102
            throw new DBException('Can not LIMIT result set by master table when GROUP BY is used');
1103
        }
1104 4
        if (count($this->order) && !isset($this->order[2])) {
1105
            throw new DBException('Can not LIMIT result set by master table with a complex ORDER BY query');
1106
        }
1107
1108 4
        $aliases = [];
1109 4
        $getAlias = function ($name) use (&$aliases) {
1110
            // to bypass use: return $name;
1111 4
            return $aliases[$name] = $aliases[$name] ?? 'alias' . static::SEP . count($aliases);
1112 4
        };
1113
        
1114 4
        $table = $this->definition->getName();
1115 4
        $relations = $this->withr;
1116 4
        foreach ($relations as $k => $v) {
1117 4
            $getAlias($k);
1118
        }
1119 4
        $w = $this->where;
1120 4
        $h = $this->having;
1121 4
        $o = $this->order;
1122 4
        $g = $this->group;
1123 4
        $j = array_map(function ($v) {
1124
            return clone $v;
1125 4
        }, $this->joins);
1126 4
        foreach ($this->definition->getRelations() as $k => $v) {
1127 4
            foreach ($w as $kk => $vv) {
1128 4
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
1129 4
                    $relations[$k] = [ $v, $table ];
1130 4
                    $w[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
1131
                }
1132
            }
1133 4
            if (isset($o[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $o[0])) {
1134 4
                $relations[$k] = [ $v, $table ];
1135 4
                $o[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[0]);
1136 4
                $o[2] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $o[2]);
1137
            }
1138 4
            foreach ($h as $kk => $vv) {
1139
                if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vv[0])) {
1140
                    $relations[$k] = [ $v, $table ];
1141
                    $h[$kk][0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vv[0]);
1142
                }
1143
            }
1144 4
            if (isset($g[0]) && preg_match('(\b'.preg_quote($k . '.'). ')i', $g[0])) {
1145
                $relations[$k] = [ $v, $table ];
1146
                $g[0] = preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $g[0]);
1147
            }
1148 4
            foreach ($j as $kk => $vv) {
1149
                foreach ($vv->keymap as $kkk => $vvv) {
1150
                    if (preg_match('(\b'.preg_quote($k . '.'). ')i', $vvv)) {
1151
                        $relations[$k] = [ $v, $table ];
1152
                        $j[$kk]->keymap[$kkk] =
1153
                            preg_replace('(\b'.preg_quote($k . '.'). ')i', $getAlias($k) . '.', $vvv);
1154
                    }
1155
                }
1156
            }
1157
        }
1158
1159 4
        $key = array_map(function ($v) use ($table) {
1160 4
            return $table . '.' . $v;
1161 4
        }, $this->pkey);
1162 4
        $own = false;
1163 4
        $dir = 'ASC';
1164 4
        if (count($o)) {
1165 4
            $dir = strpos($o[0], ' DESC') ? 'DESC' : 'ASC';
1166 4
            $own = strpos($o[2], $table . '.') === 0;
1167
        }
1168
1169 4
        $dst = $key;
1170 4
        if ($own) {
1171
            // if using own table - do not use max/min in order - that will prevent index usage
1172
            $dst[] = $o[2] . ' orderbyfix___';
1173
        } else {
1174 4
            $dst[] = 'MAX(' . $o[2] . ') orderbyfix___';
1175
        }
1176 4
        $dst = array_unique($dst);
1177
1178 4
        $par = [];
1179 4
        $sql  = 'SELECT DISTINCT '.implode(', ', $dst).' FROM '.$table.' ';
1180 4
        foreach ($j as $k => $v) {
1181
            $sql .= ($v->many ? 'LEFT ' : '' ) . 'JOIN '.$v->table->getName().' '.$k.' ON ';
1182
            $tmp = [];
1183
            foreach ($v->keymap as $kk => $vv) {
1184
                $tmp[] = $kk.' = '.$vv;
1185
            }
1186
            $sql .= implode(' AND ', $tmp) . ' ';
1187
        }
1188 4
        foreach ($relations as $k => $v) {
1189 4
            $table = $v[1] !== $this->definition->getName() ? $getAlias($v[1]) : $v[1];
1190 4
            $v = $v[0];
1191 4
            if ($v->pivot) {
1192
                $alias = $getAlias($k.'_pivot');
1193
                $sql .= 'LEFT JOIN '.$v->pivot->getName().' '.$alias.' ON ';
1194
                $tmp = [];
1195
                foreach ($v->keymap as $kk => $vv) {
1196
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
1197
                }
1198
                $sql .= implode(' AND ', $tmp) . ' ';
1199
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$getAlias($k).' ON ';
1200
                $tmp = [];
1201
                foreach ($v->pivot_keymap as $kk => $vv) {
1202
                    $tmp[] = $getAlias($k).'.'.$vv.' = '.$alias.'.'.$kk.' ';
1203
                }
1204
                $sql .= implode(' AND ', $tmp) . ' ';
1205
            } else {
1206 4
                $alias = $getAlias($k);
1207 4
                $sql .= 'LEFT JOIN '.$v->table->getName().' '.$alias.' ON ';
1208 4
                $tmp = [];
1209 4
                foreach ($v->keymap as $kk => $vv) {
1210 4
                    $tmp[] = $table.'.'.$kk.' = '.$alias.'.'.$vv.' ';
1211
                }
1212 4
                if ($v->sql) {
1213
                    $tmp[] = $v->sql . ' ';
1214
                    $par = array_merge($par, $v->par ?? []);
1215
                }
1216 4
                $sql .= implode(' AND ', $tmp) . ' ';
1217
            }
1218
        }
1219 4
        if (count($w)) {
1220 4
            $sql .= 'WHERE ';
1221 4
            $tmp = [];
1222 4
            foreach ($w as $v) {
1223 4
                $tmp[] = '(' . $v[0] . ')';
1224 4
                $par = array_merge($par, $v[1]);
1225
            }
1226 4
            $sql .= implode(' AND ', $tmp).' ';
1227
        }
1228 4
        if (!$own) {
1229 4
            $sql .= 'GROUP BY ' . implode(', ', $key) . ' ';
1230
        }
1231 4
        if (count($h)) {
1232
            $sql .= 'HAVING ';
1233
            $tmp = [];
1234
            foreach ($h as $v) {
1235
                $tmp[] = '(' . $v[0] . ')';
1236
                $par = array_merge($par, $v[1]);
1237
            }
1238
            $sql .= implode(' AND ', $tmp).' ';
1239
        }
1240 4
        if (count($o)) {
1241 4
            $sql .= 'ORDER BY ';
1242 4
            if ($own) {
1243
                $sql .= $o[2] . ' ' . $dir;
1244
            } else {
1245 4
                $sql .= 'MAX('.$o[2].') ' . $dir;
1246
            }
1247
        }
1248 4
        $porder = [];
1249 4
        $pdir = (count($o) && strpos($o[0], 'DESC') !== false) ? 'DESC' : 'ASC';
1250 4
        foreach ($this->definition->getPrimaryKey() as $field) {
1251 4
            $porder[] = $this->getColumn($field)['name'] . ' ' . $pdir;
1252
        }
1253 4
        if (count($porder)) {
1254 4
            $sql .= (count($o) ? ', ' : 'ORDER BY ') . implode(', ', $porder) . ' ';
1255
        }
1256
1257 4
        if ($this->li_of[0]) {
1258
            if ($this->db->driverName() === 'oracle') {
1259
                if ((int)$this->db->driverOption('version', 0) >= 12) {
1260
                    $sql .= 'OFFSET ' . $this->li_of[1] . ' ROWS FETCH NEXT ' . $this->li_of[0] . ' ROWS ONLY';
1261
                } else {
1262
                    $sql = "SELECT " . implode(', ', $dst) . " 
1263
                            FROM (
1264
                                SELECT tbl__.*, rownum rnum__ FROM (
1265
                                    " . $sql . "
1266
                                ) tbl__ 
1267
                                WHERE rownum <= " . ($this->li_of[0] + $this->li_of[1]) . "
1268
                            ) WHERE rnum__ > " . $this->li_of[1];
1269
                }
1270
            } else {
1271
                $sql .= 'LIMIT ' . $this->li_of[0] . ' OFFSET ' . $this->li_of[1];
1272
            }
1273
        }
1274 4
        return array_map(function ($v) {
1275 4
            if (isset($v['orderbyfix___'])) {
1276 4
                unset($v['orderbyfix___']);
1277
            }
1278 4
            return count($v) === 1 ? array_values($v)[0] : $v;
1279 4
        }, $this->db->all($sql, $par, null, false, false));
1280
    }
1281
    public function find($primary)
1282
    {
1283
        $columns = $this->definition->getPrimaryKey();
1284
        if (!count($columns)) {
1285
            throw new DBException('Missing primary key');
1286
        }
1287
        if (!is_array($primary)) {
1288
            $temp = [];
1289
            $temp[$columns[0]] = $primary;
1290
            $primary = $temp;
1291
        }
1292
        foreach ($columns as $k) {
1293
            if (!isset($primary[$k])) {
1294
                throw new DBException('Missing primary key component');
1295
            }
1296
            $this->filter($k, $primary[$k]);
1297
        }
1298
        return $this->iterator()[0];
1299
    }
1300
}
1301