Completed
Push — master ( aba273...18247e )
by Ivan
02:46
created

TableQuery   F

Complexity

Total Complexity 298

Size/Duplication

Total Lines 1268
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 66.35%

Importance

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