Completed
Push — master ( c9d76a...3de6ee )
by Ivan
04:45
created

TableQuery   F

Complexity

Total Complexity 308

Size/Duplication

Total Lines 1312
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 64.89%

Importance

Changes 0
Metric Value
wmc 308
lcom 1
cbo 7
dl 0
loc 1312
ccs 512
cts 789
cp 0.6489
rs 0.8
c 0
b 0
f 0

36 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 3
A __clone() 0 4 1
A getDefinition() 0 4 1
A offsetExists() 0 4 1
C getColumn() 0 52 11
D normalizeValue() 0 87 32
F filterSQL() 0 97 36
A filter() 0 5 2
A any() 0 13 3
A all() 0 13 3
A sort() 0 4 2
A group() 0 10 3
A paginate() 0 4 1
A __call() 0 12 4
A reset() 0 13 1
A groupBy() 0 6 1
A join() 0 15 6
A where() 0 6 1
A having() 0 6 1
A order() 0 18 4
A limit() 0 6 2
F count() 0 116 30
C columns() 0 54 13
F iterator() 0 222 64
A select() 0 4 1
B insert() 0 39 8
B update() 0 34 7
A delete() 0 20 4
A with() 0 25 4
A getIterator() 0 4 1
A offsetGet() 0 4 1
A offsetUnset() 0 4 1
A offsetSet() 0 4 1
A collection() 0 4 1
F ids() 0 184 48
A find() 0 19 5

How to fix   Complexity   

Complex Class

Complex classes like TableQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TableQuery, and based on these observations, apply Extract Interface, too.

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