GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

AbstractQueryBuilder::orderBy()   C
last analyzed

Complexity

Conditions 14
Paths 31

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 34
c 3
b 0
f 1
dl 0
loc 50
rs 6.2666
cc 14
nc 31
nop 3

How to fix   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
/**
3
 * This file is part of the O2System Framework package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @author         Steeve Andrian Salim
9
 * @copyright      Copyright (c) Steeve Andrian Salim
10
 */
11
12
// ------------------------------------------------------------------------
13
14
namespace O2System\Database\Sql\Abstracts;
15
16
// ------------------------------------------------------------------------
17
18
use O2System\Database\DataObjects\Result;
19
use O2System\Database\Sql\DataStructures\Query;
20
use O2System\Spl\Exceptions\RuntimeException;
21
22
/**
23
 * Class AbstractQueryBuilder
24
 *
25
 * @package O2System\Database\Sql\Abstracts
26
 */
27
abstract class AbstractQueryBuilder
28
{
29
    /**
30
     * AbstractQueryBuilder::testMode
31
     *
32
     * If true, no queries will actually be
33
     * ran against the database.
34
     *
35
     * @var bool
36
     */
37
    public $testMode = false;
38
39
    /**
40
     * AbstractQueryBuilder::testMode
41
     *
42
     * If true, no queries will actually be
43
     * ran against the database.
44
     *
45
     * @var bool
46
     */
47
    public $cacheMode = false;
48
49
    /**
50
     * AbstractQueryBuilder::$conn
51
     *
52
     * Query Builder database connection instance.
53
     *
54
     * @var AbstractConnection
55
     */
56
    protected $conn;
57
58
    /**
59
     * AbstractQueryBuilder::$builderCache
60
     *
61
     * Query builder cache instance.
62
     *
63
     * @var QueryBuilderCache
0 ignored issues
show
Bug introduced by
The type O2System\Database\Sql\Abstracts\QueryBuilderCache was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
64
     */
65
    protected $builderCache;
66
67
    /**
68
     * AbstractQueryBuilder::$arrayObjectConversionMethod
69
     *
70
     * Query Builder insert, update array object value conversion method.
71
     *
72
     * @var string
73
     */
74
    protected $arrayObjectConversionMethod = 'json_encode';
75
76
    /**
77
     * AbstractQueryBuilder::$SqlRandomKeywords
78
     *
79
     * ORDER BY random keyword list.
80
     *
81
     * @var array
82
     */
83
    protected $SqlOrderByRandomKeywords = ['RAND()', 'RAND(%d)'];
84
85
    /**
86
     * AbstractQueryBuilder::isSubQuery
87
     *
88
     * Is Sub Query instance flag.
89
     *
90
     * @var bool
91
     */
92
    protected $isSubQuery = false;
93
94
    // ------------------------------------------------------------------------
95
96
    /**
97
     * AbstractQueryBuilder::__construct.
98
     *
99
     * @param \O2System\Database\Sql\Abstracts\AbstractConnection $conn
100
     */
101
    public function __construct(AbstractConnection &$conn)
102
    {
103
        $this->conn =& $conn;
104
        $this->builderCache = new Query\BuilderCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new O2System\Database\Sq...es\Query\BuilderCache() of type O2System\Database\Sql\Da...ures\Query\BuilderCache is incompatible with the declared type O2System\Database\Sql\Abstracts\QueryBuilderCache of property $builderCache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
105
        $this->cacheMode = $this->conn->getConfig('cacheEnable');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->conn->getConfig('cacheEnable') can also be of type array. However, the property $cacheMode is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
106
    }
107
108
    // ------------------------------------------------------------------------
109
110
    /**
111
     * AbstractQueryBuilder::cache
112
     *
113
     * @param boolean $mode
114
     *
115
     * @return static
116
     */
117
    public function cache($mode = true)
118
    {
119
        $this->cacheEnable = (bool)$mode;
0 ignored issues
show
Bug Best Practice introduced by
The property cacheEnable does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
120
121
        return $this;
122
    }
123
124
    // ------------------------------------------------------------------------
125
126
    /**
127
     * AbstractQueryBuilder::distinct
128
     *
129
     * Sets a flag which tells the query string compiler to add DISTINCT
130
     * keyword on SELECT statement
131
     *
132
     * @param bool $distinct
133
     *
134
     * @return    static
135
     */
136
    public function distinct($distinct = true)
137
    {
138
        $this->builderCache->distinct = is_bool($distinct)
0 ignored issues
show
introduced by
The condition is_bool($distinct) is always true.
Loading history...
139
            ? $distinct
140
            : true;
141
142
        return $this;
143
    }
144
145
    // ------------------------------------------------------------------------
146
147
    /**
148
     * AbstractQueryBuilder::into
149
     *
150
     * Add SELECT INTO Sql statement portions into Query Builder.
151
     *
152
     * @param string      $table    Table name
153
     * @param string|null $database Other database name
154
     *
155
     * @return static
156
     */
157
    public function into($table, $database = null)
158
    {
159
        $this->builderCache->into = $this->conn->protectIdentifiers(
160
            $table
161
        ) . empty($database)
162
            ? ''
163
            : ' IN ' . $this->conn->escape($database);
164
165
        return $this;
166
    }
167
168
    //--------------------------------------------------------------------
169
170
    /**
171
     * AbstractQueryBuilder::union
172
     *
173
     * Add SELECT UNION Sql statement portions into Query Builder.
174
     *
175
     * @param \O2System\Database\Sql\Abstracts\AbstractQueryBuilder $select
176
     * @param bool                                                  $isUnionAll
177
     *
178
     * @return static
179
     */
180
    public function union(AbstractQueryBuilder $select, $isUnionAll = false)
181
    {
182
        $this->builderCache->store(($isUnionAll
183
            ? 'union_all'
184
            : 'union'), $select->getSqlStatement());
185
186
        return $this;
187
    }
188
189
    // ------------------------------------------------------------------------
190
191
    /**
192
     * AbstractQueryBuilder::getSqlStatement
193
     *
194
     * Gets Sql statement.
195
     *
196
     * @param bool $reset If sets TRUE the Query Builder cache will be reset.
197
     *
198
     * @return    string
199
     */
200
    public function getSqlStatement($reset = true)
201
    {
202
        $sqlStatementsSequence = [
203
            'Select',
204
            'Union',
205
            'Into',
206
            'From',
207
            'Join',
208
            'Where',
209
            'Having',
210
            'GroupBy',
211
            'OrderBy',
212
            'Limit',
213
        ];
214
215
        if (empty($this->builderCache->getStatement())) {
216
            $sqlStatement = [];
217
            foreach ($sqlStatementsSequence as $compileMethod) {
218
                $sqlStatement[] = trim(call_user_func([&$this, 'compile' . $compileMethod . 'Statement']));
219
            }
220
221
            $sqlStatement = implode(PHP_EOL, array_filter($sqlStatement));
222
223
            if ($reset) {
224
                $this->builderCache->reset();
225
226
                return $sqlStatement;
227
            }
228
229
            $this->builderCache->setStatement($sqlStatement);
230
        }
231
232
        return $this->builderCache->getStatement();
233
    }
234
235
    // ------------------------------------------------------------------------
236
237
    /**
238
     * AbstractQueryBuilder::first
239
     *
240
     * Add SELECT FIRST(field) AS alias statement
241
     *
242
     * @param string $field Input name
243
     * @param string $alias Input alias
244
     *
245
     * @return static
246
     */
247
    public function first($field, $alias = '')
248
    {
249
        return $this->prepareAggregateStatement($field, $alias, 'FIRST');
250
    }
251
252
    // ------------------------------------------------------------------------
253
254
    /**
255
     * AbstractQueryBuilder::prepareAggregateStatement
256
     *
257
     * Prepare string of Sql Aggregate Functions statement
258
     *
259
     * @param string $field Input name
260
     * @param string $alias Input alias
261
     * @param string $type  AVG|COUNT|FIRST|LAST|MAX|MIN|SUM
262
     *
263
     * @return static
264
     */
265
    protected function prepareAggregateStatement($field = '', $alias = '', $type = '')
266
    {
267
        $SqlAggregateFunctions = [
268
            'AVG'   => 'AVG(%s)', // Returns the average value
269
            'COUNT' => 'COUNT(%s)', // Returns the number of rows
270
            'FIRST' => 'FIRST(%s)', // Returns the first value
271
            'LAST'  => 'LAST(%s)', // Returns the largest value
272
            'MAX'   => 'MAX(%s)', // Returns the largest value
273
            'MIN'   => 'MIN(%s)', // Returns the smallest value
274
            'SUM'   => 'SUM(%s)' // Returns the sum
275
        ];
276
277
        if ($field !== '*' && $this->conn->protectIdentifiers) {
278
            $field = $this->conn->protectIdentifiers($field);
279
        }
280
281
        $alias = empty($alias)
282
            ? strtolower($type) . '_' . $field
283
            : $alias;
284
        $sqlStatement = sprintf($SqlAggregateFunctions[ $type ], $field)
285
            . ' AS '
286
            . $this->conn->escapeIdentifiers($alias);
287
288
        $this->select($sqlStatement);
289
290
        return $this;
291
    }
292
293
    // ------------------------------------------------------------------------
294
295
    /**
296
     * AbstractQueryBuilder::select
297
     *
298
     * Add SELECT Sql statement portions into Query Builder.
299
     *
300
     * @param string|array $field        String of field name
301
     *                                   Array list of string field names
302
     *                                   Array list of static
303
     * @param null|bool    $escape       Whether not to try to escape identifiers
304
     *
305
     * @return static
306
     */
307
    public function select($field = '*', $escape = null)
308
    {
309
        // If the escape value was not set, we will base it on the global setting
310
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
311
312
        if (is_string($field)) {
313
            $field = str_replace(' as ', ' AS ', $field);
314
315
            if (strpos($field, '+') !== false || strpos($field, '(') !== false) {
316
                $field = [$field];
317
            } else {
318
                $field = explode(',', $field);
319
            }
320
321
            foreach ($field as $name) {
322
                $name = trim($name);
323
324
                $this->builderCache->select[] = $name;
325
                $this->builderCache->noEscape[] = $escape;
326
            }
327
        } elseif (is_array($field)) {
0 ignored issues
show
introduced by
The condition is_array($field) is always true.
Loading history...
328
            foreach ($field as $fieldName => $fieldAlias) {
329
                if (is_numeric($fieldName)) {
330
                    $fieldName = $fieldAlias;
331
                } elseif (is_string($fieldName)) {
332
                    if (is_string($fieldAlias)) {
333
                        $fieldName = $fieldName . ' AS ' . $fieldAlias;
334
                    } elseif (is_array($fieldAlias)) {
335
                        $countFieldAlias = count($fieldAlias);
336
337
                        for ($i = 0; $i < $countFieldAlias; $i++) {
338
                            if ($i == 0) {
339
                                $fieldAlias[ $i ] = $fieldAlias[ $i ] . "'+";
340
                            } elseif ($i == ($countFieldAlias - 1)) {
341
                                $fieldAlias[ $i ] = "'+" . $fieldAlias[ $i ];
342
                            } else {
343
                                $fieldAlias[ $i ] = "'+" . $fieldAlias[ $i ] . "'+";
344
                            }
345
                        }
346
347
                        $fieldName = implode(', ', $fieldAlias) . ' AS ' . $fieldName;
348
                    } elseif ($fieldAlias instanceof AbstractQueryBuilder) {
349
                        $fieldName = '( ' . $fieldAlias->getSqlStatement() . ' ) AS ' . $fieldName;
350
                    }
351
                }
352
353
                $this->select($fieldName, $escape);
354
            }
355
        }
356
357
        return $this;
358
    }
359
360
    //--------------------------------------------------------------------
361
362
    /**
363
     * AbstractQueryBuilder::last
364
     *
365
     * Add SELECT LAST(field) AS alias statement
366
     *
367
     * @param string $field Input name
368
     * @param string $alias Input alias
369
     *
370
     * @return static
371
     */
372
    public function last($field, $alias = '')
373
    {
374
        return $this->prepareAggregateStatement($field, $alias, 'LAST');
375
    }
376
377
    // ------------------------------------------------------------------------
378
379
    /**
380
     * AbstractQueryBuilder::avg
381
     *
382
     * Add SELECT AVG(field) AS alias statement
383
     *
384
     * @param string $field Input name
385
     * @param string $alias Input alias
386
     *
387
     * @return static
388
     */
389
    public function avg($field, $alias = '')
390
    {
391
        return $this->prepareAggregateStatement($field, $alias, 'AVG');
392
    }
393
394
    // ------------------------------------------------------------------------
395
396
    /**
397
     * AbstractQueryBuilder::max
398
     *
399
     * Add SELECT MAX(field) AS alias statement
400
     *
401
     * @param string $field Input name
402
     * @param string $alias Input alias
403
     *
404
     * @return static
405
     */
406
    public function max($field, $alias = '')
407
    {
408
        return $this->prepareAggregateStatement($field, $alias, 'MAX');
409
    }
410
411
    // ------------------------------------------------------------------------
412
413
    /**
414
     * AbstractQueryBuilder::min
415
     *
416
     * Add SELECT MIN(field) AS alias statement
417
     *
418
     * @param string $field Input name
419
     * @param string $alias Input alias
420
     *
421
     * @return static
422
     */
423
    public function min($field, $alias = '')
424
    {
425
        return $this->prepareAggregateStatement($field, $alias, 'MIN');
426
    }
427
428
    // ------------------------------------------------------------------------
429
430
    /**
431
     * AbstractQueryBuilder::sum
432
     *
433
     * Add SELECT SUM(field) AS alias statement
434
     *
435
     * @param string $field Input name
436
     * @param string $alias Input alias
437
     *
438
     * @return static
439
     */
440
    public function sum($field, $alias = '')
441
    {
442
        return $this->prepareAggregateStatement($field, $alias, 'SUM');
443
    }
444
445
    // ------------------------------------------------------------------------
446
447
    /**
448
     * AbstractQueryBuilder::ucase
449
     *
450
     * Add SELECT UCASE(field) AS alias statement
451
     *
452
     * @see http://www.w3schools.com/Sql/Sql_func_ucase.asp
453
     *
454
     * @param string $field Input name
455
     * @param string $alias Input alias
456
     *
457
     * @return static
458
     */
459
    public function ucase($field, $alias = '')
460
    {
461
        return $this->prepareScalarStatement($field, $alias, 'UCASE');
462
    }
463
464
    // ------------------------------------------------------------------------
465
466
    /**
467
     * AbstractQueryBuilder::prepareScalarStatement
468
     *
469
     * Prepare string of Sql Scalar Functions statement
470
     *
471
     * @param string $field Input name
472
     * @param string $alias Input alias
473
     * @param string $type  UCASE|LCASE|MID|LEN|ROUND|FORMAT
474
     *
475
     * @return static
476
     */
477
    protected function prepareScalarStatement($field = '', $alias = '', $type = '')
478
    {
479
        $SqlScalarFunctions = [
480
            'UCASE'  => 'UCASE(%s)', // Converts a field to uppercase
481
            'LCASE'  => 'LCASE(%s)', // Converts a field to lowercase
482
            'LENGTH' => 'LENGTH(%s)', // Returns the length of a text field
483
        ];
484
485
        $alias = $alias === ''
486
            ? strtolower($type) . '_' . $field
487
            : $alias;
488
489
        if ($field !== '*' && $this->conn->protectIdentifiers) {
490
            $field = $this->conn->protectIdentifiers($field, true);
491
        }
492
493
        $this->select(
494
            sprintf(
495
                $SqlScalarFunctions[ $type ],
496
                $field,
497
                $this->conn->escapeIdentifiers($alias)
498
            )
499
        );
500
501
        return $this;
502
    }
503
504
    // ------------------------------------------------------------------------
505
506
    /**
507
     * AbstractQueryBuilder::lcase
508
     *
509
     * Add SELECT LCASE(field) AS alias statement
510
     *
511
     * @see http://www.w3schools.com/Sql/Sql_func_lcase.asp
512
     *
513
     * @param string $field Input name
514
     * @param string $alias Input alias
515
     *
516
     * @return static
517
     */
518
    public function lcase($field, $alias = '')
519
    {
520
        return $this->prepareScalarStatement($field, $alias, 'LCASE');
521
    }
522
523
    // ------------------------------------------------------------------------
524
525
    /**
526
     * AbstractQueryBuilder::mid
527
     *
528
     * Add SELECT MID(field) AS alias statement
529
     *
530
     * @see http://www.w3schools.com/Sql/Sql_func_mid.asp
531
     *
532
     * @param string   $field             Required. The field to extract characters from
533
     * @param int      $start             Required. Specifies the starting position (starts at 1)
534
     * @param null|int $length            Optional. The number of characters to return. If omitted, the MID() function
535
     *                                    returns the rest of the text
536
     * @param string   $alias             Input alias
537
     *
538
     * @return static
539
     */
540
    public function mid($field, $start = 1, $length = null, $alias = '')
541
    {
542
        if ($this->conn->protectIdentifiers) {
543
            $field = $this->conn->protectIdentifiers($field, true);
544
        }
545
546
        $fields = [
547
            $field,
548
            $start,
549
        ];
550
551
        if (isset($length)) {
552
            array_push($fields, intval($length));
553
        }
554
555
        $this->select(
556
            sprintf(
557
                'MID(%s)', // Extract characters from a text field
558
                implode(',', $fields)
559
            )
560
            . ' AS '
561
            . $this->conn->escapeIdentifiers(
562
                empty($alias)
563
                    ? 'mid_' . $field
564
                    : $alias
565
            )
566
        );
567
568
        return $this;
569
    }
570
571
    // ------------------------------------------------------------------------
572
573
    /**
574
     * AbstractQueryBuilder::len
575
     *
576
     * Add SELECT LEN(field) AS alias statement
577
     *
578
     * @see http://www.w3schools.com/Sql/Sql_func_len.asp
579
     *
580
     * @param string $field Input name
581
     * @param string $alias Input alias
582
     *
583
     * @return static
584
     */
585
    public function len($field, $alias = '')
586
    {
587
        return $this->prepareScalarStatement($field, $alias, 'LENGTH');
588
    }
589
590
    // ------------------------------------------------------------------------
591
592
    /**
593
     * AbstractQueryBuilder::round
594
     *
595
     * Add SELECT ROUND(field) AS alias statement
596
     *
597
     * @see http://www.w3schools.com/Sql/Sql_func_round.asp
598
     *
599
     * @param string $field    Required. The field to round.
600
     * @param int    $decimals Required. Specifies the number of decimals to be returned.
601
     * @param string $alias    Input alias
602
     *
603
     * @return static
604
     */
605
    public function round($field, $decimals = 0, $alias = '')
606
    {
607
        $this->select(
608
            sprintf(
609
                'ROUND(%s, %s)', // Rounds a numeric field to the number of decimals specified
610
                $field,
611
                $decimals
612
            )
613
            . ' AS '
614
            . $this->conn->escapeIdentifiers(
615
                empty($alias)
616
                    ? 'mid_' . $field
617
                    : $alias
618
            )
619
        );
620
621
        return $this;
622
    }
623
624
    // ------------------------------------------------------------------------
625
626
    /**
627
     * AbstractQueryBuilder::format
628
     *
629
     * Add SELECT FORMAT(field, format) AS alias statement
630
     *
631
     * @see http://www.w3schools.com/Sql/Sql_func_format.asp
632
     *
633
     * @param string $field  Input name.
634
     * @param string $format Input format.
635
     * @param string $alias  Input alias.
636
     *
637
     * @return static
638
     */
639
    public function format($field, $format, $alias = '')
640
    {
641
        $this->select(
642
            sprintf(
643
                'FORMAT(%s, %s)', // Formats how a field is to be displayed
644
                $field,
645
                $format
646
            )
647
            . ' AS '
648
            . $this->conn->escapeIdentifiers(
649
                empty($alias)
650
                    ? 'mid_' . $field
651
                    : $alias
652
            )
653
        );
654
655
        return $this;
656
    }
657
658
    // ------------------------------------------------------------------------
659
660
    /**
661
     * AbstractQueryBuilder::now
662
     *
663
     * Add / Create SELECT NOW() Sql statement.
664
     *
665
     * @return static
666
     */
667
    public function now()
668
    {
669
        $this->select('NOW()'); // Returns the current date and time
670
671
        return $this;
672
    }
673
674
    // ------------------------------------------------------------------------
675
676
    /**
677
     * AbstractQueryBuilder::extract
678
     *
679
     * Add / Create SELECT EXTRACT(unit FROM field) AS alias Sql statement
680
     *
681
     * @see http://www.w3schools.com/Sql/func_extract.asp
682
     *
683
     * @param string $field Input name
684
     * @param string $unit  UPPERCASE unit value
685
     * @param string $alias Alias field name.
686
     *
687
     * @return static|string
688
     */
689
    public function dateExtract($field, $unit, $alias = '')
690
    {
691
        $unit = strtoupper($unit);
692
693
        if (in_array($unit, $this->getDateTypes())) {
694
695
            $fieldName = $field;
696
            $fieldAlias = $alias;
697
698
            if (is_array($field)) {
0 ignored issues
show
introduced by
The condition is_array($field) is always false.
Loading history...
699
                $fieldName = key($field);
700
                $fieldAlias = $field[ $fieldName ];
701
            } elseif (strpos($field, ' AS ') !== false) {
702
                $xField = explode(' AS ', $field);
703
                $xField = array_map('trim', $xField);
704
705
                @list($fieldName, $fieldAlias) = $xField;
706
            } elseif (strpos($field, ' as ') !== false) {
707
                $xField = explode(' as ', $field);
708
                $xField = array_map('trim', $xField);
709
710
                @list($fieldName, $fieldAlias) = $xField;
711
            }
712
713
            if (strpos($fieldName, '.') !== false AND empty($fieldAlias)) {
714
                $xFieldName = explode('.', $fieldName);
715
                $xFieldName = array_map('trim', $xFieldName);
716
717
                $fieldAlias = end($xFieldName);
718
            }
719
720
            $sqlStatement = sprintf(
721
                    'EXTRACT(%s FROM %s)', // Returns a single part of a date/time
722
                    $unit,
723
                    $this->conn->protectIdentifiers($fieldName)
724
                ) . ' AS ' . $this->conn->escapeIdentifiers($fieldAlias);
0 ignored issues
show
Bug introduced by
Are you sure $this->conn->escapeIdentifiers($fieldAlias) of type array|mixed can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

724
                ) . ' AS ' . /** @scrutinizer ignore-type */ $this->conn->escapeIdentifiers($fieldAlias);
Loading history...
725
726
            $this->select($sqlStatement);
727
        }
728
729
        return $this;
730
    }
731
732
    // ------------------------------------------------------------------------
733
734
    /**
735
     * AbstractQueryBuilder::getDateTypes
736
     *
737
     * Gets Generic Sql Date Types.
738
     *
739
     * @return array
740
     */
741
    protected function getDateTypes()
742
    {
743
        return [
744
            'MICROSECOND',
745
            'SECOND',
746
            'MINUTE',
747
            'HOUR',
748
            'DAY',
749
            'WEEK',
750
            'MONTH',
751
            'QUARTER',
752
            'YEAR',
753
            'SECOND_MICROSECOND',
754
            'MINUTE_MICROSECOND',
755
            'MINUTE_SECOND',
756
            'HOUR_MICROSECOND',
757
            'HOUR_SECOND',
758
            'HOUR_MINUTE',
759
            'DAY_MICROSECOND',
760
            'DAY_SECOND',
761
            'DAY_MINUTE',
762
            'DAY_HOUR',
763
            'YEAR_MONTH',
764
        ];
765
    }
766
767
    // ------------------------------------------------------------------------
768
769
    /**
770
     * AbstractQueryBuilder::date
771
     *
772
     * Add / Create SELECT DATE(field) AS alias Sql statement
773
     *
774
     * @see http://www.w3schools.com/Sql/func_date.asp
775
     *
776
     * @param string $field Input name
777
     * @param string $alias Input name alias
778
     *
779
     * @return static|string
780
     */
781
    public function date($field, $alias = '')
782
    {
783
        $this->select(
784
            sprintf(
785
                'DATE(%s)', // Extracts the date part of a date or date/time expression
786
                $field
787
            )
788
            . ' AS '
789
            . $this->conn->escapeIdentifiers(
790
                empty($alias)
791
                    ? 'mid_' . $field
792
                    : $alias
793
            )
794
        );
795
796
        return $this;
797
    }
798
799
    // ------------------------------------------------------------------------
800
801
    /**
802
     * AbstractQueryBuilder::dateAdd
803
     *
804
     * Add / Create SELECT DATE_ADD(field, INTERVAL expression type) AS alias Sql statement
805
     *
806
     * @see http://www.w3schools.com/Sql/func_date.asp
807
     *
808
     * @param string $field    Input name
809
     * @param string $interval Number of interval expression
810
     * @param string $alias    Input alias
811
     *
812
     * @return string|static
813
     */
814
    public function dateAdd($field, $interval, $alias = '')
815
    {
816
        if ($this->hasDateType($interval)) {
817
818
            $this->select(
819
                sprintf(
820
                    'DATE_ADD(%s, INTERVAL %s)', // Adds a specified time interval to a date
821
                    $field,
822
                    $interval
823
                )
824
                . ' AS '
825
                . $this->conn->escapeIdentifiers(
826
                    empty($alias)
827
                        ? 'date_add_' . $field
828
                        : $alias
829
                )
830
            );
831
832
        }
833
834
        return $this;
835
    }
836
837
    //--------------------------------------------------------------------
838
839
    /**
840
     * AbstractQueryBuilder::hasDateType
841
     *
842
     * Validate whether the string has an Sql Date unit type
843
     *
844
     * @param $string
845
     *
846
     * @return bool
847
     */
848
    protected function hasDateType($string)
849
    {
850
        return (bool)preg_match(
851
            '/(' . implode('|\s', $this->getDateTypes()) . '\s*\(|\s)/i',
852
            trim($string)
853
        );
854
    }
855
856
    //--------------------------------------------------------------------
857
858
    /**
859
     * AbstractQueryBuilder::dateSub
860
     *
861
     * Add / Create SELECT DATE_SUB(field, INTERVAL expression type) AS alias Sql statement
862
     *
863
     * @see http://www.w3schools.com/Sql/func_date.asp
864
     *
865
     * @param string $field    Input name
866
     * @param string $interval Number of interval expression
867
     * @param string $alias    Input alias
868
     *
869
     * @return static|string
870
     */
871
    public function dateSub($field, $interval, $alias = '')
872
    {
873
        $this->select(
874
            sprintf(
875
                'DATE_SUB(%s, INTERVAL %s)', // Subtracts a specified time interval from a date
876
                $field,
877
                $interval
878
            )
879
            . ' AS '
880
            . $this->conn->escapeIdentifiers(
881
                empty($alias)
882
                    ? 'date_sub_' . $field
883
                    : $alias
884
            )
885
        );
886
887
        return $this;
888
    }
889
890
    //--------------------------------------------------------------------
891
892
    /**
893
     * AbstractQueryBuilder::dateDiff
894
     *
895
     * Add / Create SELECT DATEDIFF(datetime_start, datetime_end) AS alias Sql statement
896
     *
897
     * @see http://www.w3schools.com/Sql/func_datediff_mySql.asp
898
     *
899
     * @param array       $fields [datetime_start => datetime_end]
900
     * @param string|null $alias  Input alias
901
     *
902
     * @return static|string
903
     */
904
    public function dateDiff(array $fields, $alias)
905
    {
906
        $dateTimeStart = key($fields);
907
        $dateTimeEnd = $fields[ $dateTimeStart ];
908
909
        if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $dateTimeStart)) {
910
            $dateTimeStart = $this->conn->escape($dateTimeStart);
911
        } elseif ($this->conn->protectIdentifiers) {
912
            $dateTimeStart = $this->conn->protectIdentifiers($dateTimeStart);
913
        }
914
915
        if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $dateTimeEnd)) {
916
            $dateTimeEnd = $this->conn->escape($dateTimeEnd);
917
        } elseif ($this->conn->protectIdentifiers) {
918
            $dateTimeEnd = $this->conn->protectIdentifiers($dateTimeEnd);
919
        }
920
921
        $this->select(
922
            sprintf(
923
                'DATEDIFF(%s, %s)', // Returns the number of days between two dates
924
                $dateTimeStart,
925
                $dateTimeEnd
926
            )
927
            . ' AS '
928
            . $this->conn->escapeIdentifiers($alias)
929
        );
930
931
        return $this;
932
    }
933
934
    //--------------------------------------------------------------------
935
936
    /**
937
     * AbstractQueryBuilder::table
938
     *
939
     * Used for modifier query builder process (insert, update, replace, delete).
940
     *
941
     * @param string $table Table name.
942
     *
943
     * @return  static
944
     */
945
    public function table($table)
946
    {
947
        return $this->from($table, true);
948
    }
949
950
    //--------------------------------------------------------------------
951
952
    /**
953
     * AbstractQueryBuilder::from
954
     *
955
     * Generates FROM Sql statement portions into Query Builder.
956
     *
957
     * @param string|array $table
958
     * @param bool         $overwrite Should we remove the first table existing?
959
     *
960
     * @return  static
961
     */
962
    public function from($table, $overwrite = false)
963
    {
964
        if ($overwrite === true) {
965
            $this->builderCache->from = [];
966
            $this->builderCache->aliasedTables = [];
967
        }
968
969
        if (is_string($table)) {
970
            $table = explode(',', $table);
971
972
            foreach ($table as $name) {
973
                $name = trim($name);
974
975
                // Extract any aliases that might exist. We use this information
976
                // in the protectIdentifiers to know whether to add a table prefix
977
                $this->trackAliases($name);
978
979
                $this->builderCache->from[] = $this->conn->protectIdentifiers($name, true, null, false);
980
            }
981
        } elseif (is_array($table)) {
0 ignored issues
show
introduced by
The condition is_array($table) is always true.
Loading history...
982
            foreach ($table as $alias => $name) {
983
                $name = trim($name) . ' AS ' . trim($alias);
984
985
                // Extract any aliases that might exist. We use this information
986
                // in the protectIdentifiers to know whether to add a table prefix
987
                $this->trackAliases($name);
988
989
                $this->builderCache->from[] = $this->conn->protectIdentifiers($name, true, null, false);
990
            }
991
        }
992
993
        return $this;
994
    }
995
996
    //--------------------------------------------------------------------
997
998
    /**
999
     * AbstractQueryBuilder::trackAliases
1000
     *
1001
     * Used to track Sql statements written with aliased tables.
1002
     *
1003
     * @param string|array $table Inspected table name.
1004
     *
1005
     * @return  void
1006
     */
1007
    protected function trackAliases($table)
1008
    {
1009
        if (is_array($table)) {
1010
            foreach ($table as $name) {
1011
                $this->trackAliases($name);
1012
            }
1013
1014
            return;
1015
        }
1016
1017
        // Does the string contain a comma?  If so, we need to separate
1018
        // the string into discreet statements
1019
        if (strpos($table, ',') !== false) {
1020
            $this->trackAliases(explode(',', $table));
1021
1022
            return;
1023
        }
1024
1025
        // if a table alias is used we can recognize it by a space
1026
        if (strpos($table, ' ') !== false) {
1027
            // if the alias is written with the AS keyword, remove it
1028
            //$table = preg_replace('/\s+AS\s+/i', ' ', $table);
1029
1030
            // Grab the alias
1031
            //$table = trim(strrchr($table, ' '));
1032
            $table = str_replace([' AS ', ' as '], '.', $table);
1033
1034
            // Store the alias, if it doesn't already exist
1035
            if ( ! in_array($table, $this->builderCache->aliasedTables)) {
1036
                $this->builderCache->aliasedTables[] = $table;
1037
            }
1038
        }
1039
    }
1040
1041
    //--------------------------------------------------------------------
1042
1043
    /**
1044
     * AbstractQueryBuilder::getAliasedTables
1045
     *
1046
     * Returns list of tracked aliased tables.
1047
     *
1048
     * @return array
1049
     */
1050
    public function getAliasedTables()
1051
    {
1052
        if (empty($this->builderCache->aliasedTables)) {
1053
            return [];
1054
        }
1055
1056
        return $this->builderCache->aliasedTables;
1057
    }
1058
1059
    // --------------------------------------------------------------------
1060
1061
    /**
1062
     * AbstractQueryBuilder::join
1063
     *
1064
     * Add JOIN Sql statement portions into Query Builder.
1065
     *
1066
     * @param string    $table     Table name
1067
     * @param null      $condition Join conditions: table.column = other_table.column
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $condition is correct as it would always require null to be passed?
Loading history...
1068
     * @param string    $type      UPPERCASE join type LEFT|LEFT_OUTER|RIGHT|RIGHT_OUTER|INNER|OUTER|FULL|JOIN
1069
     * @param null|bool $escape    Whether not to try to escape identifiers
1070
     *
1071
     * @return static
1072
     */
1073
    public function join($table, $condition = null, $type = 'LEFT', $escape = null)
1074
    {
1075
        if ($type !== '') {
1076
            $type = strtoupper(trim($type));
1077
1078
            if ( ! in_array($type, ['LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'], true)) {
1079
                $type = '';
1080
            } else {
1081
                $type .= ' ';
1082
            }
1083
        }
1084
1085
        // Extract any aliases that might exist. We use this information
1086
        // in the protectIdentifiers to know whether to add a table prefix
1087
        $this->trackAliases($table);
1088
1089
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
1090
1091
        if ( ! $this->hasOperator($condition)) {
1092
            $condition = ' USING (' . ($escape
0 ignored issues
show
Bug introduced by
Are you sure $escape ? $this->conn->e...condition) : $condition of type array|mixed|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1092
            $condition = ' USING (' . (/** @scrutinizer ignore-type */ $escape
Loading history...
1093
                    ? $this->conn->escapeIdentifiers($condition)
1094
                    : $condition) . ')';
1095
        } elseif ($escape === false) {
1096
            $condition = ' ON ' . $condition;
1097
        } else {
1098
            // Split multiple conditions
1099
            if (preg_match_all('/\sAND\s|\sOR\s/i', $condition, $joints, PREG_OFFSET_CAPTURE)) {
1100
                $conditions = [];
1101
                $joints = $joints[ 0 ];
1102
                array_unshift($joints, ['', 0]);
1103
1104
                for ($i = count($joints) - 1, $pos = strlen($condition); $i >= 0; $i--) {
1105
                    $joints[ $i ][ 1 ] += strlen($joints[ $i ][ 0 ]); // offset
1106
                    $conditions[ $i ] = substr($condition, $joints[ $i ][ 1 ], $pos - $joints[ $i ][ 1 ]);
1107
                    $pos = $joints[ $i ][ 1 ] - strlen($joints[ $i ][ 0 ]);
1108
                    $joints[ $i ] = $joints[ $i ][ 0 ];
1109
                }
1110
            } else {
1111
                $conditions = [$condition];
1112
                $joints = [''];
1113
            }
1114
1115
            $condition = ' ON ';
1116
            for ($i = 0, $c = count($conditions); $i < $c; $i++) {
1117
                $operator = $this->getOperator($conditions[ $i ]);
1118
                $condition .= $joints[ $i ];
1119
                $condition .= preg_match(
1120
                    "/(\(*)?([\[\]\w\.'-]+)" . preg_quote($operator) . "(.*)/i",
1121
                    $conditions[ $i ],
1122
                    $match
1123
                )
1124
                    ? $match[ 1 ] . $this->conn->protectIdentifiers(
1125
                        $match[ 2 ]
1126
                    ) . $operator . $this->conn->protectIdentifiers($match[ 3 ])
1127
                    : $conditions[ $i ];
1128
            }
1129
        }
1130
1131
        // Do we want to escape the table name?
1132
        if ($escape === true) {
1133
            $table = $this->conn->protectIdentifiers($table, true, null, false);
1134
        }
1135
1136
        // Assemble the JOIN statement
1137
        $this->builderCache->join[] = $type . 'JOIN ' . $table . $condition;
1138
1139
        return $this;
1140
    }
1141
1142
    //--------------------------------------------------------------------
1143
1144
    /**
1145
     * AbstractQueryBuilder::hasOperator
1146
     *
1147
     * Tests whether the string has an Sql operator
1148
     *
1149
     * @param string
1150
     *
1151
     * @return    bool
1152
     */
1153
    protected function hasOperator($string)
1154
    {
1155
        return (bool)preg_match(
1156
            '/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i',
1157
            trim($string)
1158
        );
1159
    }
1160
1161
    //--------------------------------------------------------------------
1162
1163
    /**
1164
     * AbstractQueryBuilder::getOperator
1165
     *
1166
     * Returns the Sql string operator
1167
     *
1168
     * @param string
1169
     *
1170
     * @return    string
1171
     */
1172
    protected function getOperator($string)
1173
    {
1174
        static $operator;
1175
1176
        if (empty($operator)) {
1177
1178
            $likeEscapeString = ($this->conn->getConfig('likeEscapeString') !== '')
1179
                ? '\s+' . preg_quote(
1180
                    trim(
1181
                        sprintf(
1182
                            $this->conn->getConfig('likeEscapeString'),
1183
                            $this->conn->getConfig('likeEscapeCharacter')
1184
                        )
1185
                    ),
1186
                    '/'
1187
                )
1188
                : '';
1189
1190
            $operator = [
1191
                '\s*(?:<|>|!)?=\s*',             // =, <=, >=, !=
1192
                '\s*<>?\s*',                     // <, <>
1193
                '\s*>\s*',                       // >
1194
                '\s+IS NULL',                    // IS NULL
1195
                '\s+IS NOT NULL',                // IS NOT NULL
1196
                '\s+EXISTS\s*\(.*\)',        // EXISTS(Sql)
1197
                '\s+NOT EXISTS\s*\(.*\)',    // NOT EXISTS(Sql)
1198
                '\s+BETWEEN\s+',                 // BETWEEN value AND value
1199
                '\s+IN\s*\(.*\)',            // IN(list)
1200
                '\s+NOT IN\s*\(.*\)',        // NOT IN (list)
1201
                '\s+LIKE\s+\S.*(' . $likeEscapeString . ')?',    // LIKE 'expr'[ ESCAPE '%s']
1202
                '\s+NOT LIKE\s+\S.*(' . $likeEscapeString . ')?' // NOT LIKE 'expr'[ ESCAPE '%s']
1203
            ];
1204
        }
1205
1206
        return preg_match('/' . implode('|', $operator) . '/i', $string, $match)
0 ignored issues
show
Bug Best Practice introduced by
The expression return preg_match('/' . ...ch) ? $match[0] : false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
1207
            ? $match[ 0 ]
1208
            : false;
1209
    }
1210
1211
    //--------------------------------------------------------------------
1212
1213
    /**
1214
     * AbstractQueryBuilder::orWhere
1215
     *
1216
     * Add OR WHERE Sql statement portions into Query Builder
1217
     *
1218
     * @param string|array $field  Input name, array of [field => value] (grouped where)
1219
     * @param null|string  $value  Input criteria or UPPERCASE grouped type AND|OR
1220
     * @param null|bool    $escape Whether not to try to escape identifiers
1221
     *
1222
     * @return static
1223
     */
1224
    public function orWhere($field, $value = null, $escape = null)
1225
    {
1226
        return $this->prepareWhereStatement($field, $value, 'OR ', $escape, 'where');
1227
    }
1228
1229
    //--------------------------------------------------------------------
1230
1231
    /**
1232
     * AbstractQueryBuilder::prepareWhereHavingStatement
1233
     *
1234
     * Add WHERE, HAVING Sql statement portion into Query Builder.
1235
     *
1236
     * @used-by    where()
1237
     * @used-by    orWhere()
1238
     * @used-by    having()
1239
     * @used-by    orHaving()
1240
     *
1241
     * @param string    $cacheKey 'QBWhere' or 'QBHaving'
1242
     * @param mixed     $field
1243
     * @param mixed     $value
1244
     * @param string    $type
1245
     * @param null|bool $escape   Whether not to try to escape identifiers
1246
     *
1247
     * @return    static
1248
     */
1249
    protected function prepareWhereStatement($field, $value = null, $type = 'AND ', $escape = null, $cacheKey)
1250
    {
1251
        if ( ! is_array($field)) {
1252
            $field = [$field => $value];
1253
        }
1254
1255
        // If the escape value was not set will base it on the global setting
1256
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
1257
1258
        foreach ($field as $fieldName => $fieldValue) {
1259
            if ($fieldValue !== null) {
1260
                $operator = $this->getOperator($fieldName);
1261
                $fieldName = trim(str_replace($operator, '', $fieldName));
1262
1263
                $fieldBind = $this->bind($fieldName, $fieldValue);
1264
1265
                if (empty($operator) && ! in_array($cacheKey, ['between', 'notBetween'])) {
1266
                    $fieldName .= ' =';
1267
                } else {
1268
                    $fieldName .= $operator;
1269
                }
1270
            } elseif ( ! $this->hasOperator($fieldName)) {
1271
                // value appears not to have been set, assign the test to IS NULL
1272
                $fieldName .= ' IS NULL';
1273
            } elseif (preg_match('/\s*(!?=|<>|IS(?:\s+NOT)?)\s*$/i', $fieldName, $match, PREG_OFFSET_CAPTURE)) {
1274
                $fieldName = substr(
1275
                        $fieldName,
1276
                        0,
1277
                        $match[ 0 ][ 1 ]
1278
                    ) . ($match[ 1 ][ 0 ] === '='
1279
                        ? ' IS NULL'
1280
                        : ' IS NOT NULL');
1281
            } elseif ($fieldValue instanceof AbstractQueryBuilder) {
1282
                $fieldValue = $fieldValue->getSqlStatement();
1283
            }
1284
1285
            $fieldValue = ! is_null($fieldValue)
1286
                ? ' :' . $fieldBind
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldBind does not seem to be defined for all execution paths leading up to this point.
Loading history...
1287
                : $fieldValue;
1288
1289
            if ($cacheKey === 'having') {
1290
                $prefix = (count($this->builderCache->having) === 0)
1291
                    ? $this->getBracketType('')
1292
                    : $this->getBracketType($type);
1293
1294
                $this->builderCache->having[] = [
1295
                    'condition' => $prefix . $fieldName . $fieldValue,
1296
                    'escape'    => $escape,
1297
                ];
1298
            } else {
1299
                $prefix = (count($this->builderCache->where) === 0)
1300
                    ? $this->getBracketType('')
1301
                    : $this->getBracketType($type);
1302
1303
                if ($cacheKey === 'between') {
1304
                    $condition = $prefix . $fieldName . ' BETWEEN' . $fieldValue;
1305
                } elseif ($cacheKey === 'notBetween') {
1306
                    $condition = $prefix . $fieldName . ' NOT BETWEEN' . $fieldValue;
1307
                } else {
1308
                    $condition = $prefix . $fieldName . $fieldValue;
1309
                }
1310
1311
                $this->builderCache->where[] = [
1312
                    'condition' => $condition,
1313
                    'escape'    => $escape,
1314
                ];
1315
            }
1316
        }
1317
1318
        return $this;
1319
    }
1320
1321
    //--------------------------------------------------------------------
1322
1323
    /**
1324
     * AbstractQueryBuilder::binds
1325
     *
1326
     * @param $field
1327
     * @param $value
1328
     *
1329
     * @return string
1330
     */
1331
    public function bind($field, $value)
1332
    {
1333
        if ( ! array_key_exists($field, $this->builderCache->binds)) {
1334
            $this->builderCache->binds[ $field ] = $value;
1335
1336
            return $field;
1337
        }
1338
1339
        $count = 0;
1340
1341
        while (array_key_exists($field . '_' . $count, $this->builderCache->binds)) {
1342
            ++$count;
1343
        }
1344
1345
        $this->builderCache->binds[ $field . '_' . $count ] = $value;
1346
1347
        return $field . '_' . $count;
1348
    }
1349
1350
    //--------------------------------------------------------------------
1351
1352
    /**
1353
     * AbstractQueryBuilder::getBracketType
1354
     *
1355
     * @used-by    bracketOpen()
1356
     * @used-by    prepareLikeStatement()
1357
     * @used-by    whereHaving()
1358
     * @used-by    prepareWhereInStatement()
1359
     *
1360
     * @param string $type
1361
     *
1362
     * @return  string
1363
     */
1364
    protected function getBracketType($type)
1365
    {
1366
        if ($this->builderCache->bracketOpen) {
1367
            $type = '';
1368
            $this->builderCache->bracketOpen = false;
1369
        }
1370
1371
        return $type;
1372
    }
1373
1374
    //--------------------------------------------------------------------
1375
1376
    /**
1377
     * AbstractQueryBuilder::having
1378
     *
1379
     * Separates multiple calls with 'AND'.
1380
     *
1381
     * @param string    $field
1382
     * @param string    $value
1383
     * @param null|bool $escape Whether not to try to escape identifiers
1384
     *
1385
     * @return    static
1386
     */
1387
    public function having($field, $value = null, $escape = null)
1388
    {
1389
        return $this->prepareWhereStatement($field, $value, 'AND ', $escape, 'having');
1390
    }
1391
1392
    //--------------------------------------------------------------------
1393
1394
    /**
1395
     * AbstractQueryBuilder::orHaving
1396
     *
1397
     * Separates multiple calls with 'OR'.
1398
     *
1399
     * @param string    $field
1400
     * @param string    $value
1401
     * @param null|bool $escape Whether not to try to escape identifiers
1402
     *
1403
     * @return    static
1404
     */
1405
    public function orHaving($field, $value = null, $escape = null)
1406
    {
1407
        return $this->prepareWhereStatement($field, $value, 'OR ', $escape, 'having');
1408
    }
1409
1410
    //--------------------------------------------------------------------
1411
1412
    /**
1413
     * AbstractQueryBuilder::whereBetween
1414
     *
1415
     * Add WHERE BETWEEN Sql statement portions into Query Builder
1416
     *
1417
     * @param string $field  Input name
1418
     * @param array  $values Array of between values
1419
     *
1420
     * @return static
1421
     */
1422
    public function whereBetween($field, array $values = [], $escape = null)
1423
    {
1424
        return $this->prepareWhereStatement($field, implode(' AND ', $values), 'AND ', $escape, 'between');
1425
    }
1426
1427
    //--------------------------------------------------------------------
1428
1429
    /**
1430
     * AbstractQueryBuilder::orWhereBetween
1431
     *
1432
     * Add OR WHERE BETWEEN Sql statement portions into Query Builder
1433
     *
1434
     * @param string $field  Input name
1435
     * @param array  $values Array of between values
1436
     *
1437
     * @return static
1438
     */
1439
    public function orWhereBetween($field, array $values = [], $escape = null)
1440
    {
1441
        return $this->prepareWhereStatement($field, implode(' AND ', $values), 'OR ', $escape, 'between');
1442
    }
1443
1444
    //--------------------------------------------------------------------
1445
1446
    /**
1447
     * AbstractQueryBuilder::whereNotBetween
1448
     *
1449
     * Add WHERE NOT BETWEEN Sql statement portions into Query Builder
1450
     *
1451
     * @param string $field  Input name
1452
     * @param array  $values Array of between values
1453
     *
1454
     * @return static
1455
     */
1456
    public function whereNotBetween($field, array $values = [], $escape = null)
1457
    {
1458
        return $this->prepareWhereStatement($field, implode(' AND ', $values), 'OR ', $escape, 'notBetween');
1459
    }
1460
1461
    //--------------------------------------------------------------------
1462
1463
    /**
1464
     * AbstractQueryBuilder::whereIn
1465
     *
1466
     * Add WHERE IN Sql statement portions into Query Builder
1467
     *
1468
     * @param string    $field  Input name
1469
     * @param array     $values Array of values criteria
1470
     * @param null|bool $escape Whether not to try to escape identifiers
1471
     *
1472
     * @return static
1473
     */
1474
    public function whereIn($field, $values = [], $escape = null)
1475
    {
1476
        return $this->prepareWhereInStatement($field, $values, false, 'AND ', $escape);
1477
    }
1478
1479
    //--------------------------------------------------------------------
1480
1481
    /**
1482
     * AbstractQueryBuilder::prepareWhereInStatement
1483
     *
1484
     * Internal WHERE IN
1485
     *
1486
     * @used-by    WhereIn()
1487
     * @used-by    orWhereIn()
1488
     * @used-by    whereNotIn()
1489
     * @used-by    orWhereNotIn()
1490
     *
1491
     * @param string    $field  The field to search
1492
     * @param array     $values The values searched on
1493
     * @param bool      $not    If the statement would be IN or NOT IN
1494
     * @param string    $type   AND|OR
1495
     * @param null|bool $escape Whether not to try to escape identifiers
1496
     *
1497
     * @return    static
1498
     */
1499
    protected function prepareWhereInStatement(
1500
        $field = null,
1501
        $values = null,
1502
        $not = false,
1503
        $type = 'AND ',
1504
        $escape = null
1505
    ) {
1506
        if ($field === null OR $values === null) {
1507
            return $this;
1508
        }
1509
1510
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
1511
1512
        $fieldKey = $field;
1513
1514
        if (is_string($values) || is_numeric($values)) {
0 ignored issues
show
introduced by
The condition is_numeric($values) is always false.
Loading history...
1515
            $values = [$values];
1516
        }
1517
1518
        $not = ($not)
1519
            ? ' NOT'
1520
            : '';
1521
1522
        $prefix = (count($this->builderCache->where) === 0)
1523
            ? $this->getBracketType('')
1524
            : $this->getBracketType($type);
1525
1526
        if (is_array($values)) {
0 ignored issues
show
introduced by
The condition is_array($values) is always true.
Loading history...
1527
            $fieldValue = array_values($values);
1528
            $fieldBind = $this->bind($fieldKey, $fieldValue);
1529
1530
            if ($escape === true) {
1531
                $fieldKey = $this->conn->protectIdentifiers($field);
1532
            }
1533
1534
            $whereIn = [
1535
                'condition' => $prefix . $fieldKey . $not . ' IN (:' . $fieldBind . ')',
1536
                'escape'    => false,
1537
            ];
1538
1539
        } elseif ($values instanceof AbstractQueryBuilder) {
1540
1541
            if ($escape === true) {
1542
                $fieldKey = $this->conn->protectIdentifiers($field);
1543
            }
1544
1545
            $importBindsPattern = [];
1546
            $importBindsReplacement = [];
1547
            foreach ($values->builderCache->binds as $bindKey => $bindValue) {
1548
                $importBindKey = $this->bind($bindKey, $bindValue);
1549
1550
                $importBindsPattern[] = ':' . $bindKey;
1551
                $importBindsReplacement[] = ':' . $importBindKey;
1552
            }
1553
1554
            $sqlStatement = $values->getSqlStatement();
1555
            $sqlStatement = str_replace($importBindsPattern, $importBindsReplacement, $sqlStatement);
1556
1557
            $whereIn = [
1558
                'condition' => $prefix . $fieldKey . $not . ' IN (' . $sqlStatement . ')',
1559
                'escape'    => false,
1560
            ];
1561
        }
1562
1563
        if (isset($whereIn)) {
1564
            $this->builderCache->where[] = $whereIn;
1565
        }
1566
1567
        return $this;
1568
    }
1569
1570
    //--------------------------------------------------------------------
1571
1572
    /**
1573
     * AbstractQueryBuilder::orWhereIn
1574
     *
1575
     * Add OR WHERE IN Sql statement portions into Query Builder
1576
     *
1577
     * @param string    $field  Input name
1578
     * @param array     $values Array of values criteria
1579
     * @param null|bool $escape Whether not to try to escape identifiers
1580
     *
1581
     * @return static
1582
     */
1583
    public function orWhereIn($field, $values = [], $escape = null)
1584
    {
1585
        return $this->prepareWhereInStatement($field, $values, false, 'OR ', $escape);
1586
    }
1587
1588
    //--------------------------------------------------------------------
1589
1590
    /**
1591
     * AbstractQueryBuilder::whereNotIn
1592
     *
1593
     * Add WHERE NOT IN Sql statement portions into Query Builder
1594
     *
1595
     * @param string    $field  Input name
1596
     * @param array     $values Array of values criteria
1597
     * @param null|bool $escape Whether not to try to escape identifiers
1598
     *
1599
     * @return static
1600
     */
1601
    public function whereNotIn($field, $values = [], $escape = null)
1602
    {
1603
        return $this->prepareWhereInStatement($field, $values, true, 'AND ', $escape);
1604
    }
1605
1606
    //--------------------------------------------------------------------
1607
1608
    /**
1609
     * AbstractQueryBuilder::orWhereNotIn
1610
     *
1611
     * Add OR WHERE NOT IN Sql statement portions into Query Builder
1612
     *
1613
     * @param string    $field  Input name
1614
     * @param array     $values Array of values criteria
1615
     * @param null|bool $escape Whether not to try to escape identifiers
1616
     *
1617
     * @return static
1618
     */
1619
    public function orWhereNotIn($field, $values = [], $escape = null)
1620
    {
1621
        return $this->prepareWhereInStatement($field, $values, true, 'OR ', $escape);
1622
    }
1623
1624
    //--------------------------------------------------------------------
1625
1626
    /**
1627
     * AbstractQueryBuilder::orWhereNotBetween
1628
     *
1629
     * Add OR WHERE NOT BETWEEN Sql statement portions into Query Builder
1630
     *
1631
     * @param string $field  Input name
1632
     * @param array  $values Array of between values
1633
     *
1634
     * @return static
1635
     */
1636
    public function orWhereNotBetween($field, array $values = [], $escape = null)
1637
    {
1638
        return $this->prepareWhereStatement($field, implode(' OR ', $values), 'OR ', $escape, 'notBetween');
1639
    }
1640
1641
    //--------------------------------------------------------------------
1642
1643
    /**
1644
     * AbstractQueryBuilder::like
1645
     *
1646
     * Generates a %LIKE% Sql statement portions of the query.
1647
     * Separates multiple calls with 'AND'.
1648
     *
1649
     * @param string    $field         Input name
1650
     * @param string    $match         Input criteria match
1651
     * @param string    $wildcard      UPPERCASE positions of wildcard character BOTH|BEFORE|AFTER
1652
     * @param bool      $caseSensitive Whether perform case sensitive LIKE or not
1653
     * @param null|bool $escape        Whether not to try to escape identifiers
1654
     *
1655
     * @return static
1656
     */
1657
    public function like($field, $match = '', $wildcard = 'BOTH', $caseSensitive = true, $escape = null)
1658
    {
1659
        return $this->prepareLikeStatement($field, $match, 'AND ', $wildcard, '', $caseSensitive, $escape);
1660
    }
1661
1662
    //--------------------------------------------------------------------
1663
1664
    /**
1665
     * Internal LIKE
1666
     *
1667
     * @used-by    like()
1668
     * @used-by    orLike()
1669
     * @used-by    notLike()
1670
     * @used-by    orNotLike()
1671
     *
1672
     * @param mixed     $field
1673
     * @param string    $match
1674
     * @param string    $type
1675
     * @param string    $side
1676
     * @param string    $not
1677
     * @param bool      $caseSensitive IF true, will force a case-insensitive search
1678
     * @param null|bool $escape        Whether not to try to escape identifiers
1679
     *
1680
     * @return    static
1681
     */
1682
    protected function prepareLikeStatement(
1683
        $field,
1684
        $match = '',
1685
        $type = 'AND ',
1686
        $side = 'both',
1687
        $not = '',
1688
        $escape = null,
1689
        $caseSensitive = false
1690
    ) {
1691
        if ( ! is_array($field)) {
1692
            $field = [$field => $match];
1693
        }
1694
1695
        $escape = is_bool($escape)
1696
            ? $escape
1697
            : $this->conn->protectIdentifiers;
1698
1699
        // lowercase $side in case somebody writes e.g. 'BEFORE' instead of 'before' (doh)
1700
        $side = strtolower($side);
1701
1702
        foreach ($field as $fieldName => $fieldValue) {
1703
            $prefix = (count($this->builderCache->where) === 0)
1704
                ? $this->getBracketType('')
1705
                : $this->getBracketType($type);
1706
1707
            if ($caseSensitive === true) {
1708
                $fieldValue = strtolower($fieldValue);
1709
            }
1710
1711
            if ($side === 'none') {
1712
                $bind = $this->bind($fieldName, $fieldValue);
1713
            } elseif ($side === 'before') {
1714
                $bind = $this->bind($fieldName, "%$fieldValue");
1715
            } elseif ($side === 'after') {
1716
                $bind = $this->bind($fieldName, "$fieldValue%");
1717
            } else {
1718
                $bind = $this->bind($fieldName, "%$fieldValue%");
1719
            }
1720
1721
            $likeStatement = $this->platformPrepareLikeStatement($prefix, $fieldName, $not, $bind, $caseSensitive);
1722
1723
            // some platforms require an escape sequence definition for LIKE wildcards
1724
            if ($escape === true && $this->conn->getConfig('likeEscapeString') !== '') {
1725
                $likeStatement .= sprintf(
1726
                    $this->conn->getConfig('likeEscapeString'),
1727
                    $this->conn->getConfig('likeEscapeCharacter')
1728
                );
1729
            }
1730
1731
            $this->builderCache->where[] = ['condition' => $likeStatement, 'escape' => $escape];
1732
        }
1733
1734
        return $this;
1735
    }
1736
1737
    //--------------------------------------------------------------------
1738
1739
    /**
1740
     * AbstractQueryBuilder::platformPrepareLikeStatement
1741
     *
1742
     * Platform independent LIKE statement builder.
1743
     *
1744
     * @param string|null $prefix
1745
     * @param string      $column
1746
     * @param string|null $not
1747
     * @param string      $bind
1748
     * @param bool        $caseSensitive
1749
     *
1750
     * @return string
1751
     */
1752
    abstract protected function platformPrepareLikeStatement(
1753
        $prefix = null,
1754
        $column,
1755
        $not = null,
1756
        $bind,
1757
        $caseSensitive = false
1758
    );
1759
1760
    //--------------------------------------------------------------------
1761
1762
    /**
1763
     * AbstractQueryBuilder::orLike
1764
     *
1765
     * Add OR LIKE Sql statement portions into Query Builder
1766
     *
1767
     * @param string    $field         Input name
1768
     * @param string    $match         Input criteria match
1769
     * @param string    $wildcard      UPPERCASE positions of wildcard character BOTH|BEFORE|AFTER
1770
     * @param bool      $caseSensitive Whether perform case sensitive LIKE or not
1771
     * @param null|bool $escape        Whether not to try to escape identifiers
1772
     *
1773
     * @return static
1774
     */
1775
    public function orLike($field, $match = '', $wildcard = 'BOTH', $caseSensitive = true, $escape = null)
1776
    {
1777
        return $this->prepareLikeStatement($field, $match, 'OR ', $wildcard, '', $caseSensitive, $escape);
1778
    }
1779
1780
    //--------------------------------------------------------------------
1781
1782
    /**
1783
     * AbstractQueryBuilder::notLike
1784
     *
1785
     * Add NOT LIKE Sql statement portions into Query Builder
1786
     *
1787
     * @param string    $field         Input name
1788
     * @param string    $match         Input criteria match
1789
     * @param string    $wildcard      UPPERCASE positions of wildcard character BOTH|BEFORE|AFTER
1790
     * @param bool      $caseSensitive Whether perform case sensitive LIKE or not
1791
     * @param null|bool $escape        Whether not to try to escape identifiers
1792
     *
1793
     * @return static
1794
     */
1795
    public function notLike($field, $match = '', $wildcard = 'BOTH', $caseSensitive = true, $escape = null)
1796
    {
1797
        return $this->prepareLikeStatement($field, $match, 'AND ', $wildcard, 'NOT', $caseSensitive, $escape);
1798
    }
1799
1800
    //--------------------------------------------------------------------
1801
1802
    /**
1803
     * AbstractQueryBuilder::orNotLike
1804
     *
1805
     * Add OR NOT LIKE Sql statement portions into Query Builder
1806
     *
1807
     * @param string    $field         Input name
1808
     * @param string    $match         Input criteria match
1809
     * @param string    $wildcard      UPPERCASE positions of wildcard character BOTH|BEFORE|AFTER
1810
     * @param bool      $caseSensitive Whether perform case sensitive LIKE or not
1811
     * @param null|bool $escape        Whether not to try to escape identifiers
1812
     *
1813
     * @return static
1814
     */
1815
    public function orNotLike($field, $match = '', $wildcard = 'BOTH', $caseSensitive = true, $escape = null)
1816
    {
1817
        return $this->prepareLikeStatement($field, $match, 'OR ', $wildcard, 'NOT', $caseSensitive, $escape);
1818
    }
1819
1820
    //--------------------------------------------------------------------
1821
1822
    /**
1823
     * AbstractQueryBuilder::groupBy
1824
     *
1825
     * Add GROUP BY Sql statement into Query Builder.
1826
     *
1827
     * @param string    $field
1828
     * @param null|bool $escape Whether not to try to escape identifiers
1829
     *
1830
     * @return $this
1831
     */
1832
    public function groupBy($field, $escape = null)
1833
    {
1834
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
1835
1836
        if (is_string($field)) {
0 ignored issues
show
introduced by
The condition is_string($field) is always true.
Loading history...
1837
            $field = ($escape === true)
1838
                ? explode(',', $field)
1839
                : [$field];
1840
        }
1841
1842
        foreach ($field as $fieldName) {
1843
            $fieldName = trim($fieldName);
1844
1845
            if ($fieldName !== '') {
1846
                $fieldName = ['field' => $fieldName, 'escape' => $escape];
1847
1848
                $this->builderCache->groupBy[] = $fieldName;
1849
            }
1850
        }
1851
1852
        return $this;
1853
    }
1854
1855
    //--------------------------------------------------------------------
1856
1857
    /**
1858
     * AbstractQueryBuilder::orderBy
1859
     *
1860
     * Add ORDER BY Sql statement portions into Query Builder.
1861
     *
1862
     * @param string|array $fields
1863
     * @param string       $direction
1864
     * @param null|bool    $escape Whether not to try to escape identifiers
1865
     *
1866
     * @return $this
1867
     */
1868
    public function orderBy($fields, $direction = 'ASC', $escape = null)
1869
    {
1870
        $orderBy = [];
1871
        $direction = strtoupper(trim($direction));
1872
1873
        if ($direction === 'RANDOM') {
1874
            $direction = '';
1875
1876
            // Do we have a seed value?
1877
            $fields = ctype_digit((string)$fields)
1878
                ? sprintf($this->SqlOrderByRandomKeywords[ 1 ], $fields)
0 ignored issues
show
Bug introduced by
It seems like $fields can also be of type array; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1878
                ? sprintf($this->SqlOrderByRandomKeywords[ 1 ], /** @scrutinizer ignore-type */ $fields)
Loading history...
1879
                : $this->SqlOrderByRandomKeywords[ 0 ];
1880
        } elseif (empty($fields)) {
1881
            return $this;
1882
        } elseif ($direction !== '') {
1883
            $direction = in_array($direction, ['ASC', 'DESC'], true)
1884
                ? ' ' . $direction
1885
                : '';
1886
        }
1887
1888
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
1889
1890
        if ($escape === false) {
1891
            $orderBy[] = ['field' => $fields, 'direction' => $direction, 'escape' => false];
1892
        } elseif (is_array($fields)) {
1893
            foreach ($fields as $field_name => $field_direction) {
1894
                $field_direction = is_numeric($field_name) ? $direction : $field_direction;
1895
                $orderBy[] = ['field' => trim($field_name), 'direction' => $field_direction, 'escape' => true];
1896
            }
1897
        } else {
1898
            foreach (explode(',', $fields) as $fields) {
0 ignored issues
show
introduced by
$fields is overwriting one of the parameters of this function.
Loading history...
1899
                $orderBy[] = ($direction === ''
1900
                    && preg_match(
1901
                        '/\s+(ASC|DESC)$/i',
1902
                        rtrim($fields),
1903
                        $match,
1904
                        PREG_OFFSET_CAPTURE
1905
                    ))
1906
                    ? [
1907
                        'field'     => ltrim(substr($fields, 0, $match[ 0 ][ 1 ])),
1908
                        'direction' => ' ' . $match[ 1 ][ 0 ],
1909
                        'escape'    => true,
1910
                    ]
1911
                    : ['field' => trim($fields), 'direction' => $direction, 'escape' => true];
1912
            }
1913
        }
1914
1915
        $this->builderCache->orderBy = array_merge($this->builderCache->orderBy, $orderBy);
1916
1917
        return $this;
1918
    }
1919
1920
    // --------------------------------------------------------------------
1921
1922
    /**
1923
     * AbstractQueryBuilder::page
1924
     *
1925
     * Add Set LIMIT, OFFSET Sql statement by page number and entries.
1926
     *
1927
     * @param int  $page  Page number
1928
     * @param null $limit Num entries of each page
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $limit is correct as it would always require null to be passed?
Loading history...
1929
     *
1930
     * @return static
1931
     */
1932
    public function page($page = 1, $limit = null)
1933
    {
1934
        $page = (int)intval($page);
1935
1936
        $limit = (int)(isset($limit)
1937
            ? $limit
1938
            : ($this->builderCache->limit === false
1939
                ? 5
1940
                : $this->builderCache->limit
1941
            )
1942
        );
1943
1944
        $offset = ($page - 1) * $limit;
1945
1946
        $this->limit($limit, $offset);
1947
1948
        return $this;
1949
    }
1950
1951
    //--------------------------------------------------------------------
1952
1953
    /**
1954
     * AbstractQueryBuilder::limit
1955
     *
1956
     * Add LIMIT,OFFSET Sql statement into Query Builder.
1957
     *
1958
     * @param int $limit  LIMIT value
1959
     * @param int $offset OFFSET value
1960
     *
1961
     * @return    static
1962
     */
1963
    public function limit($limit, $offset = null)
1964
    {
1965
        if ( ! is_null($limit)) {
0 ignored issues
show
introduced by
The condition is_null($limit) is always false.
Loading history...
1966
            $this->builderCache->limit = (int)$limit;
1967
        }
1968
1969
        $this->offset($offset);
1970
1971
        return $this;
1972
    }
1973
1974
    //--------------------------------------------------------------------
1975
1976
    /**
1977
     * AbstractQueryBuilder::offset
1978
     *
1979
     * Add OFFSET Sql statement into Query Builder.
1980
     *
1981
     * @param int $offset OFFSET value
1982
     *
1983
     * @return    static
1984
     */
1985
    public function offset($offset)
1986
    {
1987
        if ( ! empty($offset)) {
1988
            $this->builderCache->offset = (int)$offset;
1989
        }
1990
1991
        return $this;
1992
    }
1993
1994
    //--------------------------------------------------------------------
1995
1996
    /**
1997
     * AbstractQueryBuilder::get
1998
     *
1999
     * Perform execution of Sql Query Builder and run ConnectionInterface::query()
2000
     *
2001
     * @param null|int $limit
2002
     * @param null|int $offset
2003
     *
2004
     * @return Result
2005
     * @throws \O2System\Spl\Exceptions\RuntimeException
2006
     * @throws \Psr\Cache\InvalidArgumentException
2007
     */
2008
    public function get($limit = null, $offset = null)
2009
    {
2010
        if ( ! empty($limit)) {
2011
            $this->limit($limit, $offset);
2012
        }
2013
2014
        if ($this->cacheMode) {
2015
            $this->conn->cache($this->cacheMode);
2016
        }
2017
2018
        $result = $this->testMode
2019
            ? $this->getSqlStatement(false)
2020
            : $this->conn->query($this->getSqlStatement(false), $this->builderCache->binds);
2021
2022
        if ($result) {
2023
            $result->setNumPerPage($this->builderCache->limit);
2024
            $result->setNumFounds($this->countAllResults(false));
2025
            $result->setNumTotal($this->countAll(false));
2026
        }
2027
2028
        $this->builderCache->reset();
2029
2030
        $this->cacheMode = $this->conn->getConfig('cacheEnable');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->conn->getConfig('cacheEnable') can also be of type array. However, the property $cacheMode is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2031
2032
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type boolean|string which is incompatible with the documented return type O2System\Database\DataObjects\Result.
Loading history...
2033
    }
2034
2035
    //--------------------------------------------------------------------
2036
2037
    /**
2038
     * AbstractQueryBuilder::countAllResult
2039
     *
2040
     * Perform execution of count all result from Query Builder along with WHERE, LIKE, HAVING, GROUP BY, and LIMIT Sql
2041
     * statement.
2042
     *
2043
     * @return int
2044
     * @throws \O2System\Spl\Exceptions\RuntimeException
2045
     * @throws \Psr\Cache\InvalidArgumentException
2046
     */
2047
    public function countAllResults($reset = true)
2048
    {
2049
        // save previous
2050
        $previousSelect = $this->builderCache->select;
2051
        $previousLimit = $this->builderCache->limit;
2052
        $previousOffset = $this->builderCache->offset;
2053
2054
        // add select counter
2055
        array_unshift($this->builderCache->select, 'COUNT(*) AS numrows');
2056
2057
        // generate Sql statement
2058
        $sqlStatement = $this->getSqlStatement();
2059
2060
        // restore previous select
2061
        $this->builderCache->select = $previousSelect;
2062
        $this->builderCache->limit = $previousLimit;
2063
        $this->builderCache->offset = $previousOffset;
2064
2065
        if ($this->testMode) {
2066
            return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type integer.
Loading history...
2067
        } elseif ($this->isSubQuery) {
2068
            $statement = new Query\Statement();
2069
            $statement->setSqlStatement($sqlStatement, $this->builderCache->binds);
2070
            $statement->setSqlFinalStatement($this->conn->compileSqlBinds($sqlStatement,
2071
                $this->builderCache->binds));
2072
2073
            $sqlStatement = $statement->getSqlFinalStatement();
2074
2075
            return '( ' . $sqlStatement . ' )';
0 ignored issues
show
Bug Best Practice introduced by
The expression return '( ' . $sqlStatement . ' )' returns the type string which is incompatible with the documented return type integer.
Loading history...
2076
        }
2077
2078
        $numrows = 0;
2079
        if($result = $this->conn->query($sqlStatement, $this->builderCache->binds)) {
2080
            if($result->count()) {
2081
                $numrows = $result->first()->numrows;
0 ignored issues
show
Bug Best Practice introduced by
The property numrows does not exist on O2System\Database\DataObjects\Result\Row. Since you implemented __get, consider adding a @property annotation.
Loading history...
2082
            }
2083
        }
2084
2085
        if($reset) {
2086
            $this->builderCache->reset();
2087
        }
2088
2089
        return $numrows;
2090
    }
2091
2092
    //--------------------------------------------------------------------
2093
2094
    /**
2095
     * AbstractQueryBuilder::getWhere
2096
     *
2097
     * Perform execution of Sql Query Builder and run ConnectionInterface::query()
2098
     *
2099
     * @param array    $where
2100
     * @param null|int $limit
2101
     * @param null|int $offset
2102
     *
2103
     * @return Result
2104
     * @throws \O2System\Spl\Exceptions\RuntimeException
2105
     * @throws \Psr\Cache\InvalidArgumentException
2106
     */
2107
    public function getWhere(array $where = [], $limit = null, $offset = null)
2108
    {
2109
        $this->where($where);
2110
2111
        if ( ! empty($limit)) {
2112
            $this->limit($limit, $offset);
2113
        }
2114
2115
        if ($this->cacheMode) {
2116
            $this->conn->cache($this->cacheMode);
2117
        }
2118
2119
        $result = $this->testMode
2120
            ? $this->getSqlStatement()
2121
            : $this->conn->query($this->getSqlStatement(false), $this->builderCache->binds);
2122
2123
        if ($result) {
2124
            $result->setNumPerPage($this->builderCache->limit);
2125
            $result->setNumFounds($this->countAllResults(false));
2126
            $result->setNumTotal($this->countAll(false));
2127
        }
2128
2129
        $this->builderCache->reset();
2130
2131
        $this->cacheMode = $this->conn->getConfig('cacheEnable');
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->conn->getConfig('cacheEnable') can also be of type array. However, the property $cacheMode is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2132
2133
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type boolean|string which is incompatible with the documented return type O2System\Database\DataObjects\Result.
Loading history...
2134
    }
2135
2136
    //--------------------------------------------------------------------
2137
2138
    /**
2139
     * AbstractQueryBuilder::where
2140
     *
2141
     * Add WHERE Sql statement portions into Query Builder
2142
     *
2143
     * @param string|array $field  Input name, array of [field => value] (grouped where)
2144
     * @param null|string  $value  Input criteria or UPPERCASE grouped type AND|OR
2145
     * @param null|bool    $escape Whether not to try to escape identifiers
2146
     *
2147
     * @return static
2148
     */
2149
    public function where($field, $value = null, $escape = null)
2150
    {
2151
        return $this->prepareWhereStatement($field, $value, 'AND ', $escape, 'where');
2152
    }
2153
2154
    //--------------------------------------------------------------------
2155
2156
    /**
2157
     * AbstractQueryBuilder::countAll
2158
     *
2159
     * Perform execution of count all records of a table.
2160
     *
2161
     * @access  public
2162
     * @return int
2163
     * @throws \O2System\Spl\Exceptions\RuntimeException
2164
     * @throws \Psr\Cache\InvalidArgumentException
2165
     */
2166
    public function countAll($reset = true)
2167
    {
2168
        // save previous cache
2169
        $previousQueryBuilderCache = clone $this->builderCache;
2170
2171
        // reset cache
2172
        $this->builderCache->reset();
2173
2174
        $this->builderCache->from = $previousQueryBuilderCache->from;
2175
2176
        $this->count('*', 'numrows');
2177
2178
        // generate Sql statement
2179
        $sqlStatement = $this->getSqlStatement();
2180
2181
        // restore previous cache
2182
        $this->builderCache = $previousQueryBuilderCache;
2183
        unset($previousQueryBuilderCache);
2184
2185
        if ($this->testMode) {
2186
            return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type integer.
Loading history...
2187
        } elseif ($this->isSubQuery) {
2188
            $statement = new Query\Statement();
2189
            $statement->setSqlStatement($sqlStatement, $this->builderCache->binds);
2190
            $statement->setSqlFinalStatement($this->conn->compileSqlBinds($sqlStatement,
2191
                $this->builderCache->binds));
2192
2193
            $sqlStatement = $statement->getSqlFinalStatement();
2194
2195
            return '( ' . $sqlStatement . ' )';
0 ignored issues
show
Bug Best Practice introduced by
The expression return '( ' . $sqlStatement . ' )' returns the type string which is incompatible with the documented return type integer.
Loading history...
2196
        }
2197
2198
        $numrows = 0;
2199
        if($result = $this->conn->query($sqlStatement, $this->builderCache->binds)) {
2200
            if($result->count()) {
2201
                $numrows = $result->first()->numrows;
0 ignored issues
show
Bug Best Practice introduced by
The property numrows does not exist on O2System\Database\DataObjects\Result\Row. Since you implemented __get, consider adding a @property annotation.
Loading history...
2202
            }
2203
        }
2204
2205
        if($reset) {
2206
            $this->builderCache->reset();
2207
        }
2208
2209
        return $numrows;
2210
    }
2211
2212
    //--------------------------------------------------------------------
2213
2214
    /**
2215
     * AbstractQueryBuilder::count
2216
     *
2217
     * Add SELECT COUNT(field) AS alias statement
2218
     *
2219
     * @param string $field Input name
2220
     * @param string $alias Input alias
2221
     *
2222
     * @return static
2223
     */
2224
    public function count($field, $alias = '')
2225
    {
2226
        return $this->prepareAggregateStatement($field, $alias, 'COUNT');
2227
    }
2228
2229
    //--------------------------------------------------------------------
2230
2231
    /**
2232
     * AbstractQueryBuilder::orBracketOpen
2233
     *
2234
     * Starts a query group, but ORs the group
2235
     *
2236
     * @return    static
2237
     */
2238
    public function orBracketOpen()
2239
    {
2240
        return $this->bracketOpen('', 'OR ');
2241
    }
2242
2243
    //--------------------------------------------------------------------
2244
2245
    /**
2246
     * AbstractQueryBuilder::bracketOpen
2247
     *
2248
     * Starts a query group.
2249
     *
2250
     * @param string $not  (Internal use only)
2251
     * @param string $type (Internal use only)
2252
     *
2253
     * @return    static
2254
     */
2255
    public function bracketOpen($not = '', $type = 'AND ')
2256
    {
2257
        $type = $this->getBracketType($type);
2258
2259
        $this->builderCache->bracketOpen = true;
2260
        $prefix = count($this->builderCache->where) === 0
2261
            ? ''
2262
            : $type;
2263
        $where = [
2264
            'condition' => $prefix . $not . str_repeat(' ', ++$this->builderCache->bracketCount) . ' (',
2265
            'escape'    => false,
2266
        ];
2267
2268
        $this->builderCache->where[] = $where;
2269
2270
        return $this;
2271
    }
2272
2273
    //--------------------------------------------------------------------
2274
2275
    /**
2276
     * AbstractQueryBuilder::notBracketOpen
2277
     *
2278
     * Starts a query group, but NOTs the group
2279
     *
2280
     * @return    static
2281
     */
2282
    public function notBracketOpen()
2283
    {
2284
        return $this->bracketOpen('NOT ', 'AND ');
2285
    }
2286
2287
    //--------------------------------------------------------------------
2288
2289
    /**
2290
     * AbstractQueryBuilder::orNotBracketOpen
2291
     *
2292
     * Starts a query group, but OR NOTs the group
2293
     *
2294
     * @return    static
2295
     */
2296
    public function orNotBracketOpen()
2297
    {
2298
        return $this->bracketOpen('NOT ', 'OR ');
2299
    }
2300
2301
    //--------------------------------------------------------------------
2302
2303
    /**
2304
     * AbstractQueryBuilder::bracketClose
2305
     *
2306
     * Ends a query group
2307
     *
2308
     * @return    static
2309
     */
2310
    public function bracketClose()
2311
    {
2312
        $this->builderCache->bracketOpen = false;
2313
2314
        $where = [
2315
            'condition' => str_repeat(' ', $this->builderCache->bracketCount--) . ')',
2316
            'escape'    => false,
2317
        ];
2318
2319
        $this->builderCache->where[] = $where;
2320
2321
        return $this;
2322
    }
2323
2324
    //--------------------------------------------------------------------
2325
2326
    /**
2327
     * AbstractQueryBuilder::insert
2328
     *
2329
     * Execute INSERT Sql Query
2330
     *
2331
     * @param array $sets     An associative array of set values.
2332
     *                        sets[][field => value]
2333
     * @param bool  $escape   Whether to escape values and identifiers
2334
     *
2335
     * @return bool
2336
     * @throws \O2System\Spl\Exceptions\RuntimeException
2337
     * @throws \Psr\Cache\InvalidArgumentException
2338
     */
2339
    public function insert(array $sets, $escape = null)
2340
    {
2341
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2342
2343
        $this->set($sets, null, $escape);
2344
2345
        $result = false;
2346
2347
        if (count($this->builderCache->sets)) {
2348
            $sqlStatement = $this->platformInsertStatement(
2349
                $this->conn->protectIdentifiers(
2350
                    $this->builderCache->from[ 0 ],
2351
                    true,
2352
                    $escape,
2353
                    false
2354
                ),
2355
                array_keys($this->builderCache->sets),
2356
                array_values($this->builderCache->sets)
2357
            );
2358
2359
            if ($this->testMode) {
2360
                return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type boolean.
Loading history...
2361
            }
2362
2363
            $result = $this->conn->query($sqlStatement, $this->builderCache->binds);
2364
        }
2365
2366
        $this->builderCache->resetModifier();
2367
2368
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type O2System\Database\DataObjects\Result which is incompatible with the documented return type boolean.
Loading history...
2369
    }
2370
2371
    //--------------------------------------------------------------------
2372
2373
    /**
2374
     * AbstractQueryBuilder::set
2375
     *
2376
     * Allows key/value pairs to be set for inserting or updating
2377
     *
2378
     * @param string|array $field
2379
     * @param mixed        $value
2380
     * @param null         $escape
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $escape is correct as it would always require null to be passed?
Loading history...
2381
     *
2382
     * @return static|array
2383
     */
2384
    public function set($field, $value = '', $escape = null)
2385
    {
2386
        $field = $this->objectToArray($field);
2387
2388
        if ( ! is_array($field)) {
0 ignored issues
show
introduced by
The condition is_array($field) is always true.
Loading history...
2389
            $field = [$field => $value];
2390
        }
2391
2392
        $escape = is_bool($escape)
0 ignored issues
show
introduced by
The condition is_bool($escape) is always false.
Loading history...
2393
            ? $escape
2394
            : $this->conn->protectIdentifiers;
2395
2396
        foreach ($field as $key => $value) {
2397
            if ($key === 'birthday' || $key === 'date') {
2398
                if (is_array($value)) {
2399
                    $value = $value[ 'year' ] . '-' . $value[ 'month' ] . '-' . $value[ 'date' ];
2400
                } elseif (is_object($value)) {
2401
                    $value = $value->year . '-' . $value->month . '-' . $value->date;
2402
                }
2403
            } elseif (is_array($value) || is_object($value)) {
2404
                $value = call_user_func_array($this->arrayObjectConversionMethod, [$value]);
2405
            }
2406
2407
            $this->builderCache->binds[ $key ] = $value;
2408
            $this->builderCache->sets[ $this->conn->protectIdentifiers($key, false,
2409
                $escape) ] = ':' . $key;
2410
        }
2411
2412
        return $this;
2413
    }
2414
2415
    //--------------------------------------------------------------------
2416
2417
    /**
2418
     * AbstractQueryBuilder::objectToArray
2419
     *
2420
     * Takes an object as input and converts the class variables to array key/vals
2421
     *
2422
     * @param mixed $object
2423
     *
2424
     * @return  array
2425
     */
2426
    protected function objectToArray($object)
2427
    {
2428
        if ( ! is_object($object)) {
2429
            return $object;
2430
        }
2431
2432
        $array = [];
2433
        foreach (get_object_vars($object) as $key => $value) {
2434
            // There are some built in keys we need to ignore for this conversion
2435
            if ( ! is_object($value) && ! is_array($value) && $key !== '_parent_name') {
2436
                $array[ $key ] = $value;
2437
            }
2438
        }
2439
2440
        return $array;
2441
    }
2442
2443
    //--------------------------------------------------------------------
2444
2445
    /**
2446
     * AbstractQueryBuilder::platformInsertStatement
2447
     *
2448
     * Generates a platform-specific insert string from the supplied data.
2449
     *
2450
     * @param string $table  Table name.
2451
     * @param array  $keys   Insert keys.
2452
     * @param array  $values Insert values.
2453
     *
2454
     * @return string
2455
     */
2456
    abstract protected function platformInsertStatement($table, array $keys, array $values);
2457
2458
    //--------------------------------------------------------------------
2459
2460
    /**
2461
     * AbstractQueryBuilder::insertBatch
2462
     *
2463
     * Execute INSERT batch Sql Query
2464
     *
2465
     * @param array $sets        An associative array of set values.
2466
     *                           sets[][field => value]
2467
     * @param int   $batchSize   Maximum batch size
2468
     * @param bool  $escape      Whether to escape values and identifiers
2469
     *
2470
     * @return bool
2471
     * @throws \Psr\Cache\InvalidArgumentException
2472
     * @throws \O2System\Spl\Exceptions\RuntimeException
2473
     */
2474
    public function insertBatch(array $sets, $batchSize = 1000, $escape = null)
2475
    {
2476
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2477
2478
        $this->setInsertReplaceBatch($sets);
2479
2480
        // Batch this baby
2481
        $affectedRows = 0;
2482
        for ($i = 0, $total = count($sets); $i < $total; $i += $batchSize) {
2483
            $Sql = $this->platformInsertBatchStatement(
2484
                $this->conn->protectIdentifiers($this->builderCache->from[ 0 ], true, $escape, false),
2485
                $this->builderCache->keys,
2486
                array_slice($this->builderCache->sets, $i, $batchSize)
2487
            );
2488
2489
            if ($this->testMode) {
2490
                ++$affectedRows;
2491
            } else {
2492
                $this->conn->query($Sql, $this->builderCache->binds);
2493
                $affectedRows += $this->conn->getAffectedRows();
2494
            }
2495
        }
2496
2497
        if ( ! $this->testMode) {
2498
            $this->builderCache->resetModifier();
2499
        }
2500
2501
        return $affectedRows;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $affectedRows returns the type integer which is incompatible with the documented return type boolean.
Loading history...
2502
    }
2503
2504
    //--------------------------------------------------------------------
2505
2506
    /**
2507
     * AbstractQueryBuilder::setInsertBatch
2508
     *
2509
     * The "setInsertBatch" function.  Allows key/value pairs to be set for batch inserts
2510
     *
2511
     * @param mixed  $field
2512
     * @param string $value
2513
     * @param bool   $escape Whether to escape values and identifiers
2514
     *
2515
     * @return  void
2516
     */
2517
    protected function setInsertReplaceBatch($field, $value = '', $escape = null)
2518
    {
2519
        $field = $this->batchObjectToArray($field);
2520
2521
        if ( ! is_array($field)) {
0 ignored issues
show
introduced by
The condition is_array($field) is always true.
Loading history...
2522
            $field = [$field => $value];
2523
        }
2524
2525
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2526
2527
        $rowKeys = array_keys($this->objectToArray(current($field)));
2528
        sort($rowKeys);
2529
2530
        foreach ($field as $row) {
2531
            $row = $this->objectToArray($row);
2532
            if (count(array_diff($rowKeys, array_keys($row))) > 0
2533
                || count(
2534
                    array_diff(array_keys($row), $rowKeys)
2535
                ) > 0
2536
            ) {
2537
                // batch function above returns an error on an empty array
2538
                $this->builderCache->sets[] = [];
2539
2540
                return;
2541
            }
2542
2543
            ksort($row); // puts $row in the same order as our keys
2544
2545
            $clean = [];
2546
            foreach ($row as $key => $value) {
2547
                $clean[] = ':' . $this->bind($key, $value);
2548
            }
2549
2550
            $row = $clean;
2551
2552
            $this->builderCache->sets[] = '(' . implode(',', $row) . ')';
2553
        }
2554
2555
        foreach ($rowKeys as $rowKey) {
2556
            $this->builderCache->keys[] = $this->conn->protectIdentifiers($rowKey, false, $escape);
2557
        }
2558
    }
2559
2560
    //--------------------------------------------------------------------
2561
2562
    /**
2563
     * Object to Array
2564
     *
2565
     * Takes an object as input and converts the class variables to array key/vals
2566
     *
2567
     * @param object
2568
     *
2569
     * @return    array
2570
     */
2571
    protected function batchObjectToArray($object)
2572
    {
2573
        if ( ! is_object($object)) {
2574
            return $object;
2575
        }
2576
2577
        $array = [];
2578
        $out = get_object_vars($object);
2579
        $fields = array_keys($out);
2580
2581
        foreach ($fields as $field) {
2582
            // There are some built in keys we need to ignore for this conversion
2583
            if ($field !== '_parent_name') {
2584
                $i = 0;
2585
                foreach ($out[ $field ] as $data) {
2586
                    $array[ $i++ ][ $field ] = $data;
2587
                }
2588
            }
2589
        }
2590
2591
        return $array;
2592
    }
2593
2594
    //--------------------------------------------------------------------
2595
2596
    /**
2597
     * AbstractQueryBuilder::platformInsertBatchStatement
2598
     *
2599
     * @param string $table
2600
     * @param array  $keys
2601
     * @param array  $values
2602
     *
2603
     * @return mixed
2604
     */
2605
    abstract protected function platformInsertBatchStatement($table, array $keys, array $values);
2606
2607
    //--------------------------------------------------------------------
2608
2609
    /**
2610
     * AbstractQueryBuilder::replace
2611
     *
2612
     * Compiles an replace into string and runs the query
2613
     *
2614
     * @param array $sets     An associative array of set values.
2615
     *                        sets[][field => value]
2616
     * @param bool  $escape   Whether to escape values and identifiers
2617
     *
2618
     * @return bool
2619
     * @throws \Psr\Cache\InvalidArgumentException
2620
     * @throws \O2System\Spl\Exceptions\RuntimeException
2621
     */
2622
    public function replace(array $sets, $escape = null)
2623
    {
2624
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2625
2626
        $this->set($sets, null, $escape);
2627
2628
        if (count($this->builderCache->sets)) {
2629
            $sqlStatement = $this->platformReplaceStatement(
2630
                $this->conn->protectIdentifiers(
2631
                    $this->builderCache->from[ 0 ],
2632
                    true,
2633
                    $escape,
2634
                    false
2635
                ),
2636
                array_keys($this->builderCache->sets),
2637
                array_values($this->builderCache->sets)
2638
            );
2639
2640
            if ($this->testMode) {
2641
                return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type boolean.
Loading history...
2642
            }
2643
2644
            $sqlBinds = $this->builderCache->binds;
2645
            $this->builderCache->resetModifier();
2646
2647
            return $this->conn->query($sqlStatement, $sqlBinds);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conn->quer...qlStatement, $sqlBinds) also could return the type O2System\Database\DataObjects\Result which is incompatible with the documented return type boolean.
Loading history...
2648
        }
2649
2650
        return false;
2651
    }
2652
2653
    //--------------------------------------------------------------------
2654
2655
    /**
2656
     * AbstractQueryBuilder::platformReplaceStatement
2657
     *
2658
     * Generates a platform-specific update string from the supplied data.
2659
     *
2660
     * @param string $table  Table name.
2661
     * @param array  $keys   Insert keys.
2662
     * @param array  $values Insert values.
2663
     *
2664
     * @return string
2665
     */
2666
    abstract protected function platformReplaceStatement($table, array $keys, array $values);
2667
2668
    //--------------------------------------------------------------------
2669
2670
    /**
2671
     * AbstractQueryBuilder::replaceBatch
2672
     *
2673
     * Execute REPLACE batch Sql Query
2674
     *
2675
     * @param array $sets        An associative array of set values.
2676
     *                           sets[][field => value]
2677
     * @param int   $batchSize   Maximum batch size
2678
     * @param bool  $escape      Whether to escape values and identifiers
2679
     *
2680
     * @return bool
2681
     * @throws \Psr\Cache\InvalidArgumentException
2682
     * @throws \O2System\Spl\Exceptions\RuntimeException
2683
     */
2684
    public function replaceBatch(array $sets, $batchSize = 1000, $escape = null)
2685
    {
2686
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2687
2688
        $this->setInsertReplaceBatch($sets);
2689
2690
        // Batch this baby
2691
        $affectedRows = 0;
2692
        for ($i = 0, $total = count($sets); $i < $total; $i += $batchSize) {
2693
            $Sql = $this->platformReplaceStatement(
2694
                $this->conn->protectIdentifiers($this->builderCache->from[ 0 ], true, $escape, false),
2695
                $this->builderCache->keys,
2696
                array_slice($this->builderCache->sets, $i, $batchSize)
2697
            );
2698
2699
            if ($this->testMode) {
2700
                ++$affectedRows;
2701
            } else {
2702
                $this->conn->query($Sql, $this->builderCache->binds);
2703
                $affectedRows += $this->conn->getAffectedRows();
2704
            }
2705
        }
2706
2707
        if ( ! $this->testMode) {
2708
            $this->builderCache->resetModifier();
2709
        }
2710
2711
        return $affectedRows;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $affectedRows returns the type integer which is incompatible with the documented return type boolean.
Loading history...
2712
    }
2713
2714
    //--------------------------------------------------------------------
2715
2716
    /**
2717
     * AbstractQueryBuilder::update
2718
     *
2719
     * Compiles an update string and runs the query.
2720
     *
2721
     * @param array $sets      An associative array of set values.
2722
     *                         sets[][field => value]
2723
     * @param array $where     WHERE [field => match]
2724
     * @param bool  $escape    Whether to escape values and identifiers
2725
     *
2726
     * @return bool
2727
     * @throws \Psr\Cache\InvalidArgumentException
2728
     * @throws \O2System\Spl\Exceptions\RuntimeException
2729
     */
2730
    public function update(array $sets, array $where = [], $escape = null)
2731
    {
2732
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2733
2734
        $this->set($sets, null, $escape);
2735
        $this->where($where);
2736
2737
        if (count($this->builderCache->sets) && count($this->builderCache->from)) {
2738
            $sqlStatement = $this->platformUpdateStatement(
2739
                $this->conn->protectIdentifiers(
2740
                    $this->builderCache->from[ 0 ],
2741
                    true,
2742
                    $escape,
2743
                    false
2744
                ),
2745
                $this->builderCache->sets
2746
            );
2747
2748
            if ($this->testMode) {
2749
                return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type boolean.
Loading history...
2750
            }
2751
2752
            $sqlBinds = $this->builderCache->binds;
2753
            $this->builderCache->resetModifier();
2754
2755
            return $this->conn->query($sqlStatement, $sqlBinds);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conn->quer...qlStatement, $sqlBinds) also could return the type O2System\Database\DataObjects\Result which is incompatible with the documented return type boolean.
Loading history...
2756
        }
2757
2758
        return false;
2759
    }
2760
2761
    //--------------------------------------------------------------------
2762
2763
    /**
2764
     * AbstractQueryBuilder::platformUpdateStatement
2765
     *
2766
     * Generates a platform-specific update string from the supplied data.
2767
     *
2768
     * @param string $table    Table name.
2769
     * @param array  $sets     An associative array of set values.
2770
     *                         sets[][field => value]
2771
     *
2772
     * @return string
2773
     */
2774
    abstract protected function platformUpdateStatement($table, array $sets);
2775
2776
    //--------------------------------------------------------------------
2777
2778
    /**
2779
     * AbstractQueryBuilder::updateBatch
2780
     *
2781
     * Execute UPDATE batch Sql Query
2782
     *
2783
     * @param array  $sets      Array of data sets[][field => value]
2784
     * @param string $index     Index field
2785
     * @param int    $batchSize Maximum batch size
2786
     * @param bool   $escape    Whether to escape values and identifiers
2787
     *
2788
     * @return bool
2789
     * @throws \Psr\Cache\InvalidArgumentException
2790
     * @throws \O2System\Spl\Exceptions\RuntimeException
2791
     */
2792
    public function updateBatch(array $sets, $index = null, $batchSize = 1000, $escape = null)
2793
    {
2794
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2795
2796
        $this->setUpdateBatch($sets, $index);
2797
2798
        $affectedRows = 0;
2799
        for ($i = 0, $total = count($this->builderCache->sets); $i < $total; $i += $batchSize) {
2800
            $sql = $this->platformUpdateBatchStatement(
2801
                $this->builderCache->from[ 0 ],
2802
                array_slice($this->builderCache->sets, $i, $batchSize),
2803
                $this->conn->protectIdentifiers($index, false, $escape, false)
2804
            );
2805
2806
            if ($this->testMode) {
2807
                ++$affectedRows;
2808
            } else {
2809
                $this->conn->query($sql, $this->builderCache->binds);
2810
                $affectedRows += $this->conn->getAffectedRows();
2811
            }
2812
2813
            $this->builderCache[ 'where' ] = [];
2814
        }
2815
2816
        if ( ! $this->testMode) {
2817
            $this->builderCache->resetModifier();
2818
        }
2819
2820
        return $affectedRows;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $affectedRows returns the type integer which is incompatible with the documented return type boolean.
Loading history...
2821
    }
2822
2823
    //--------------------------------------------------------------------
2824
2825
    /**
2826
     * AbstractQueryBuilder::setUpdateBatch
2827
     *
2828
     * The "setUpdateBatch" function.  Allows key/value pairs to be set for batch updating
2829
     *
2830
     * @param mixed  $sets
2831
     * @param string $index
2832
     * @param bool   $escape
2833
     *
2834
     * @return static
2835
     * @throws \O2System\Spl\Exceptions\RuntimeException
2836
     */
2837
    protected function setUpdateBatch(array $sets, $index = '', $escape = null)
2838
    {
2839
        $sets = $this->batchObjectToArray($sets);
2840
2841
        if ( ! is_array($sets)) {
0 ignored issues
show
introduced by
The condition is_array($sets) is always true.
Loading history...
2842
            // @todo error
2843
        }
2844
2845
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2846
2847
        foreach ($sets as $set) {
2848
            $indexSet = false;
2849
            $row = [];
2850
            foreach ($set as $key => $value) {
2851
                if ($key === $index) {
2852
                    $indexSet = true;
2853
                }
2854
2855
                $bind = $this->bind($key, $value);
2856
2857
                $row[ $this->conn->protectIdentifiers($key, false, $escape) ] = ':' . $bind;
2858
            }
2859
2860
            if ($indexSet === false) {
2861
                // 'One or more rows submitted for batch updating is missing the specified index.'
2862
                throw new RuntimeException('E_DATABASE_BATCH_UPDATE_MISSING_INDEX');
2863
            }
2864
2865
            $this->builderCache->sets[] = $row;
2866
        }
2867
2868
        return $this;
2869
    }
2870
2871
    /**
2872
     * AbstractQueryBuilder::platformUpdateBatchStatement
2873
     *
2874
     * Generates a platform-specific batch update string from the supplied data.
2875
     *
2876
     * @param string $table  Table name
2877
     * @param array  $values Update data
2878
     * @param string $index  WHERE key
2879
     *
2880
     * @return    string
2881
     */
2882
    abstract protected function platformUpdateBatchStatement($table, $values, $index);
2883
2884
    //--------------------------------------------------------------------
2885
2886
    /**
2887
     * AbstractQueryBuilder::delete
2888
     *
2889
     * Compiles a delete string and runs the query
2890
     *
2891
     * @param array $where Where clause.
2892
     * @param int   $limit Limit clause.
2893
     *
2894
     * @return string
2895
     * @throws \O2System\Spl\Exceptions\RuntimeException
2896
     * @throws \Psr\Cache\InvalidArgumentException
2897
     */
2898
    public function delete($where = [], $limit = null)
2899
    {
2900
        $this->where($where);
2901
2902
        if (isset($limit)) {
2903
            $this->limit($limit);
2904
        }
2905
2906
        $sqlStatement = $this->platformDeleteStatement(
2907
            $this->conn->protectIdentifiers(
2908
                $this->builderCache->from[ 0 ],
2909
                true,
2910
                $this->conn->protectIdentifiers,
2911
                false
2912
            )
2913
        );
2914
2915
        if ($this->testMode) {
2916
            return $sqlStatement;
2917
        }
2918
2919
        $sqlBinds = $this->builderCache->binds;
2920
        $this->builderCache->resetModifier();
2921
2922
        return $this->conn->query($sqlStatement, $sqlBinds);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conn->quer...qlStatement, $sqlBinds) also could return the type boolean which is incompatible with the documented return type string.
Loading history...
2923
    }
2924
2925
    //--------------------------------------------------------------------
2926
2927
    /**
2928
     * AbstractQueryBuilder::platformDeleteStatement
2929
     *
2930
     * Generates a platform-specific delete string from the supplied data
2931
     *
2932
     * @param string $table The table name.
2933
     *
2934
     * @return  string
2935
     */
2936
    abstract protected function platformDeleteStatement($table);
2937
2938
    //--------------------------------------------------------------------
2939
2940
    /**
2941
     * AbstractQueryBuilder::flush
2942
     *
2943
     * Compiles a truncate string and runs the query
2944
     * If the database does not support the truncate() command
2945
     * This function maps to "DELETE FROM table"
2946
     *
2947
     * @param bool $escape Whether to table identifiers
2948
     *
2949
     * @return bool TRUE on success, FALSE on failure
2950
     * @throws \O2System\Spl\Exceptions\RuntimeException
2951
     * @throws \Psr\Cache\InvalidArgumentException
2952
     */
2953
    public function flush($table, $escape = null)
2954
    {
2955
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2956
2957
        if ($escape) {
2958
            $table = $this->conn->protectIdentifiers($table, true, true);
2959
        }
2960
2961
        $sqlStatement = $this->platformDeleteStatement($table);
2962
2963
        if ($this->testMode === true) {
2964
            return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type boolean.
Loading history...
2965
        }
2966
2967
        $this->builderCache->resetModifier();
2968
2969
        return $this->conn->query($sqlStatement);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conn->query($sqlStatement) also could return the type O2System\Database\DataObjects\Result which is incompatible with the documented return type boolean.
Loading history...
2970
    }
2971
2972
    //--------------------------------------------------------------------
2973
2974
    /**
2975
     * AbstractQueryBuilder::truncate
2976
     *
2977
     * Compiles a truncate string and runs the query
2978
     * If the database does not support the truncate() command
2979
     * This function maps to "DELETE FROM table"
2980
     *
2981
     * @param bool $escape Whether to table identifiers
2982
     *
2983
     * @return bool TRUE on success, FALSE on failure
2984
     * @throws \O2System\Spl\Exceptions\RuntimeException
2985
     * @throws \Psr\Cache\InvalidArgumentException
2986
     */
2987
    public function truncate($table, $escape = null)
2988
    {
2989
        is_bool($escape) || $escape = $this->conn->protectIdentifiers;
2990
2991
        if ($escape) {
2992
            $table = $this->conn->protectIdentifiers($table, true, true);
2993
        }
2994
2995
        $sqlStatement = $this->platformTruncateStatement($table);
2996
2997
        if ($this->testMode === true) {
2998
            return $sqlStatement;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $sqlStatement returns the type string which is incompatible with the documented return type boolean.
Loading history...
2999
        }
3000
3001
        $this->builderCache->resetModifier();
3002
3003
        return $this->conn->query($sqlStatement);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conn->query($sqlStatement) also could return the type O2System\Database\DataObjects\Result which is incompatible with the documented return type boolean.
Loading history...
3004
    }
3005
3006
    //--------------------------------------------------------------------
3007
3008
    /**
3009
     * AbstractQueryBuilder::platformTruncateStatement
3010
     *
3011
     * Generates a platform-specific truncate statement.
3012
     *
3013
     * @param string    the table name
0 ignored issues
show
Bug introduced by
The type O2System\Database\Sql\Abstracts\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
3014
     *
3015
     * @return    string
3016
     */
3017
    abstract protected function platformTruncateStatement($table);
3018
3019
    //--------------------------------------------------------------------
3020
3021
    /**
3022
     * AbstractQueryBuilder::binds
3023
     *
3024
     * @param array $binds
3025
     *
3026
     * @return static
3027
     */
3028
    public function binds(array $binds)
3029
    {
3030
        foreach ($binds as $field => $value) {
3031
            $this->bind($field, $value);
3032
        }
3033
3034
        return $this;
3035
    }
3036
3037
    //--------------------------------------------------------------------
3038
3039
    /**
3040
     * AbstractQueryBuilder::subQuery
3041
     *
3042
     * Performs Query Builder sub query mode.
3043
     *
3044
     * @return \O2System\Database\Sql\Abstracts\AbstractQueryBuilder
3045
     */
3046
    public function subQuery()
3047
    {
3048
        $subQuery = clone $this;
3049
        $subQuery->builderCache = new Query\BuilderCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new O2System\Database\Sq...es\Query\BuilderCache() of type O2System\Database\Sql\Da...ures\Query\BuilderCache is incompatible with the declared type O2System\Database\Sql\Abstracts\QueryBuilderCache of property $builderCache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3050
3051
        $subQuery->isSubQuery = true;
3052
3053
        return $subQuery;
3054
    }
3055
3056
    // ------------------------------------------------------------------------
3057
3058
    /**
3059
     * AbstractQueryBuilder::compileSelectStatement
3060
     *
3061
     * Compile the SELECT statement
3062
     *
3063
     * Generates a query string based on which functions were used.
3064
     * Should not be called directly.
3065
     *
3066
     * @param bool $selectOverride
3067
     *
3068
     * @return    string
3069
     */
3070
    protected function compileSelectStatement($selectOverride = false)
3071
    {
3072
        // Write the "select" portion of the query
3073
        if ($selectOverride !== false) {
3074
            $sqlStatement = $selectOverride;
3075
        } else {
3076
            $sqlStatement = ( ! $this->builderCache->distinct)
3077
                ? 'SELECT %s'
3078
                : 'SELECT DISTINCT %s';
3079
3080
            if (count($this->builderCache->select) === 0) {
3081
                $SqlSelectStatement = "*";
3082
            } else {
3083
                // Cycle through the "select" portion of the query and prep each column name.
3084
                // The reason we protect identifiers here rather than in the select() function
3085
                // is because until the user calls the from() function we don't know if there are aliases
3086
                foreach ($this->builderCache->select as $selectKey => $selectField) {
3087
                    $noEscape = isset($this->builderCache->noEscape [ $selectKey ])
3088
                        ? $this->builderCache->noEscape [ $selectKey ]
3089
                        : null;
3090
                    $this->builderCache->select [ $selectKey ] = $this->conn->protectIdentifiers(
3091
                        $selectField,
3092
                        false,
3093
                        $noEscape
3094
                    );
3095
                }
3096
3097
                $SqlSelectStatement = implode(", \n\t", $this->builderCache->select);
3098
            }
3099
3100
            $sqlStatement = sprintf($sqlStatement, $SqlSelectStatement);
3101
        }
3102
3103
        return trim($sqlStatement);
0 ignored issues
show
Bug introduced by
It seems like $sqlStatement can also be of type true; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3103
        return trim(/** @scrutinizer ignore-type */ $sqlStatement);
Loading history...
3104
    }
3105
3106
    //--------------------------------------------------------------------
3107
3108
    /**
3109
     * AbstractQueryBuilder::compileIntoStatement
3110
     *
3111
     * @return string
3112
     */
3113
    protected function compileUnionStatement()
3114
    {
3115
        $sqlStatement = '';
3116
3117
        if (count($this->builderCache->union)) {
3118
            foreach ($this->builderCache->union as $union) {
3119
                $sqlStatement .= "\n UNION \n" . $union;
3120
            }
3121
        }
3122
3123
        if (count($this->builderCache->unionAll)) {
3124
            foreach ($this->builderCache->unionAll as $union) {
3125
                $sqlStatement .= "\n UNION ALL \n" . $union;
3126
            }
3127
        }
3128
3129
        return trim($sqlStatement);
3130
    }
3131
3132
    //--------------------------------------------------------------------
3133
3134
    /**
3135
     * AbstractQueryBuilder::compileIntoStatement
3136
     *
3137
     * @return string
3138
     */
3139
    protected function compileIntoStatement()
3140
    {
3141
        return "\n" . $this->builderCache->into;
3142
    }
3143
3144
    //--------------------------------------------------------------------
3145
3146
    /**
3147
     * AbstractQueryBuilder::compileFromStatement
3148
     *
3149
     * @return string
3150
     */
3151
    protected function compileFromStatement()
3152
    {
3153
        if (count($this->builderCache->from) > 0) {
3154
            return "\n" . sprintf(
3155
                    'FROM %s',
3156
                    implode(',', array_unique($this->builderCache->from))
3157
                );
3158
        }
3159
    }
3160
3161
    //--------------------------------------------------------------------
3162
3163
    /**
3164
     * AbstractQueryBuilder::compileJoinStatement
3165
     *
3166
     * @return string
3167
     */
3168
    protected function compileJoinStatement()
3169
    {
3170
        if (count($this->builderCache->join) > 0) {
3171
            return "\n" . implode("\n", $this->builderCache->join);
3172
        }
3173
    }
3174
3175
    //--------------------------------------------------------------------
3176
3177
    /**
3178
     * AbstractQueryBuilder::compileWhereStatement
3179
     *
3180
     * @return string
3181
     */
3182
    protected function compileWhereStatement()
3183
    {
3184
        return $this->compileWhereHavingStatement('where');
3185
    }
3186
3187
    //--------------------------------------------------------------------
3188
3189
    /**
3190
     * AbstractQueryBuilder::compileWhereHavingStatement
3191
     *
3192
     * Compile WHERE, HAVING statements
3193
     *
3194
     * Escapes identifiers in WHERE and HAVING statements at execution time.
3195
     *
3196
     * Required so that aliases are tracked properly, regardless of whether
3197
     * where(), orWhere(), having(), orHaving are called prior to from(),
3198
     * join() and prefixTable is added only if needed.
3199
     *
3200
     * @param string $cacheKey 'QBWhere' or 'QBHaving'
3201
     *
3202
     * @return    string    Sql statement
3203
     */
3204
    protected function compileWhereHavingStatement($cacheKey)
3205
    {
3206
        if (count($this->builderCache->{$cacheKey}) > 0) {
3207
            for ($i = 0, $c = count($this->builderCache->{$cacheKey}); $i < $c; $i++) {
3208
                // Is this condition already compiled?
3209
                if (is_string($this->builderCache->{$cacheKey}[ $i ])) {
3210
                    continue;
3211
                } elseif ($this->builderCache->{$cacheKey}[ $i ][ 'escape' ] === false) {
3212
                    $this->builderCache->{$cacheKey}[ $i ]
3213
                        = $this->builderCache->{$cacheKey}[ $i ][ 'condition' ];
3214
                    continue;
3215
                }
3216
3217
                // Split multiple conditions
3218
                $conditions = preg_split(
3219
                    '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i',
3220
                    $this->builderCache->{$cacheKey}[ $i ][ 'condition' ],
3221
                    -1,
3222
                    PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
3223
                );
3224
3225
                for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci++) {
0 ignored issues
show
Bug introduced by
It seems like $conditions can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3225
                for ($ci = 0, $cc = count(/** @scrutinizer ignore-type */ $conditions); $ci < $cc; $ci++) {
Loading history...
3226
                    if (($op = $this->getOperator($conditions[ $ci ])) === false
3227
                        OR
3228
                        ! preg_match(
3229
                            '/^(\(?)(.*)(' . preg_quote($op, '/') . ')\s*(.*(?<!\)))?(\)?)$/i',
3230
                            $conditions[ $ci ],
3231
                            $matches
3232
                        )
3233
                    ) {
3234
                        continue;
3235
                    }
3236
3237
                    // $matches = array(
3238
                    //  0 => '(test <= foo)',   /* the whole thing */
3239
                    //  1 => '(',       /* optional */
3240
                    //  2 => 'test',        /* the field name */
3241
                    //  3 => ' <= ',        /* $op */
3242
                    //  4 => 'foo',     /* optional, if $op is e.g. 'IS NULL' */
3243
                    //  5 => ')'        /* optional */
3244
                    // );
3245
3246
                    if ( ! empty($matches[ 4 ])) {
3247
                        //$this->isLiteral($matches[4]) OR $matches[4] = $this->protectIdentifiers(trim($matches[4]));
3248
                        $matches[ 4 ] = ' ' . $matches[ 4 ];
3249
                    }
3250
3251
                    $conditions[ $ci ] = $matches[ 1 ] . $this->conn->protectIdentifiers(trim($matches[ 2 ]))
3252
                        . ' ' . trim($matches[ 3 ]) . $matches[ 4 ] . $matches[ 5 ];
3253
                }
3254
3255
                $this->builderCache->{$cacheKey}[ $i ] = implode('', $conditions);
0 ignored issues
show
Bug introduced by
It seems like $conditions can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3255
                $this->builderCache->{$cacheKey}[ $i ] = implode('', /** @scrutinizer ignore-type */ $conditions);
Loading history...
3256
            }
3257
3258
            if ($cacheKey === 'having') {
3259
                return "\n" . sprintf(
3260
                        'HAVING %s',
3261
                        implode("\n", $this->builderCache->having)
3262
                    );
3263
            }
3264
3265
            return "\n" . sprintf(
3266
                    'WHERE %s',
3267
                    implode("\n", $this->builderCache->{$cacheKey})
3268
                );
3269
        }
3270
3271
        return '';
3272
    }
3273
3274
    //--------------------------------------------------------------------
3275
3276
    /**
3277
     * AbstractQueryBuilder::compileGroupByStatement
3278
     *
3279
     * Compile GROUP BY
3280
     *
3281
     * Escapes identifiers in GROUP BY statements at execution time.
3282
     *
3283
     * Required so that aliases are tracked properly, regardless of wether
3284
     * groupBy() is called prior to from(), join() and prefixTable is added
3285
     * only if needed.
3286
     *
3287
     * @return    string    Sql statement
3288
     */
3289
    protected function compileGroupByStatement()
3290
    {
3291
        if (count($this->builderCache->groupBy) > 0) {
3292
            for ($i = 0, $c = count($this->builderCache->groupBy); $i < $c; $i++) {
3293
                // Is it already compiled?
3294
                if (is_string($this->builderCache->groupBy[ $i ])) {
3295
                    continue;
3296
                }
3297
3298
                $this->builderCache->groupBy[ $i ] = ($this->builderCache->groupBy[ $i ][ 'escape' ]
3299
                    === false OR
3300
                    $this->isLiteral(
3301
                        $this->builderCache->groupBy[ $i ][ 'field' ]
3302
                    ))
3303
                    ? $this->builderCache->groupBy[ $i ][ 'field' ]
3304
                    : $this->conn->protectIdentifiers($this->builderCache->groupBy[ $i ][ 'field' ]);
3305
            }
3306
3307
            return "\n" . sprintf(
3308
                    'GROUP BY %s',
3309
                    implode(', ', $this->builderCache->groupBy)
3310
                );
3311
        }
3312
3313
        return '';
3314
    }
3315
3316
    //--------------------------------------------------------------------
3317
3318
    /**
3319
     * AbstractQueryBuilder::isLiteral
3320
     *
3321
     * Determines if a string represents a literal value or a field name
3322
     *
3323
     * @param string $string
3324
     *
3325
     * @return    bool
3326
     */
3327
    protected function isLiteral($string)
3328
    {
3329
        $string = trim($string);
3330
3331
        if (empty($string) || ctype_digit($string) || (string)(float)$string === $string
3332
            || in_array(
3333
                strtoupper($string),
3334
                ['TRUE', 'FALSE'],
3335
                true
3336
            )
3337
        ) {
3338
            return true;
3339
        }
3340
3341
        static $stringArray;
3342
3343
        if (empty($stringArray)) {
3344
            $stringArray = ($this->conn->getConfig('escapeCharacter') !== '"')
3345
                ? ['"', "'"]
3346
                : ["'"];
3347
        }
3348
3349
        return in_array($string[ 0 ], $stringArray, true);
3350
    }
3351
3352
    //--------------------------------------------------------------------
3353
3354
    /**
3355
     * AbstractQueryBuilder::compileHavingStatement
3356
     *
3357
     * @return string
3358
     */
3359
    protected function compileHavingStatement()
3360
    {
3361
        return $this->compileWhereHavingStatement('having');
3362
    }
3363
3364
    //--------------------------------------------------------------------
3365
3366
    /**
3367
     * AbstractQueryBuilder::compileHavingStatement
3368
     *
3369
     * @return string
3370
     */
3371
    protected function compileBetweenStatement()
3372
    {
3373
        return $this->compileWhereHavingStatement('between');
3374
    }
3375
3376
    //--------------------------------------------------------------------
3377
3378
    /**
3379
     * AbstractQueryBuilder::compileHavingStatement
3380
     *
3381
     * @return string
3382
     */
3383
    protected function compileNotBetweenStatement()
3384
    {
3385
        return $this->compileWhereHavingStatement('notBetween');
3386
    }
3387
3388
    //--------------------------------------------------------------------
3389
3390
    /**
3391
     * AbstractQueryBuilder::compileOrderByStatement
3392
     *
3393
     * Compile ORDER BY
3394
     *
3395
     * Escapes identifiers in ORDER BY statements at execution time.
3396
     *
3397
     * Required so that aliases are tracked properly, regardless of wether
3398
     * orderBy() is called prior to from(), join() and prefixTable is added
3399
     * only if needed.
3400
     *
3401
     * @return    string    Sql statement
3402
     */
3403
    protected function compileOrderByStatement()
3404
    {
3405
        if (is_array($this->builderCache->orderBy) && count($this->builderCache->orderBy) > 0) {
3406
            for ($i = 0, $c = count($this->builderCache->orderBy); $i < $c; $i++) {
3407
                if ($this->builderCache->orderBy[ $i ][ 'escape' ] !== false
3408
                    && ! $this->isLiteral(
3409
                        $this->builderCache->orderBy[ $i ][ 'field' ]
3410
                    )
3411
                ) {
3412
                    $this->builderCache->orderBy[ $i ][ 'field' ] = $this->conn->protectIdentifiers(
3413
                        $this->builderCache->orderBy[ $i ][ 'field' ]
3414
                    );
3415
                }
3416
3417
                $this->builderCache->orderBy[ $i ] = $this->builderCache->orderBy[ $i ][ 'field' ]
3418
                    . $this->builderCache->orderBy[ $i ][ 'direction' ];
3419
            }
3420
3421
            return $this->builderCache->orderBy = "\n" . sprintf(
3422
                    'ORDER BY %s',
3423
                    implode(', ', $this->builderCache->orderBy)
3424
                );
3425
        } elseif (is_string($this->builderCache->orderBy)) {
3426
            return $this->builderCache->orderBy;
3427
        }
3428
3429
        return '';
3430
    }
3431
3432
    //--------------------------------------------------------------------
3433
3434
    /**
3435
     * AbstractQueryBuilder::compileLimitStatement
3436
     *
3437
     * @return string
3438
     */
3439
    protected function compileLimitStatement()
3440
    {
3441
        if ($this->builderCache->limit) {
3442
            if ($this->builderCache->offset) {
3443
                return sprintf(
3444
                    'LIMIT %s OFFSET %s',
3445
                    $this->builderCache->limit,
3446
                    $this->builderCache->offset
3447
                );
3448
            }
3449
3450
            return "\n" . sprintf(
3451
                    'LIMIT %s',
3452
                    $this->builderCache->limit
3453
                );
3454
        }
3455
    }
3456
}
3457