Completed
Push — master ( 793829...85d2a2 )
by Ivan
03:56
created

TableQuery::columns()   C

Complexity

Conditions 13
Paths 61

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 55.5807

Importance

Changes 0
Metric Value
dl 0
loc 54
ccs 14
cts 38
cp 0.3684
rs 6.6166
c 0
b 0
f 0
cc 13
nc 61
nop 2
crap 55.5807

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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