• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 12673986434

08 Jan 2025 03:42PM UTC coverage: 84.455% (+0.001%) from 84.454%
12673986434

Pull #9385

github

web-flow
Merge 06e47f0ee into e475fd8fa
Pull Request #9385: refactor: Fix phpstan expr.resultUnused

20699 of 24509 relevant lines covered (84.45%)

190.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.73
/system/Database/BaseBuilder.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Database;
15

16
use Closure;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\Exceptions\DataException;
19
use CodeIgniter\Exceptions\InvalidArgumentException;
20
use CodeIgniter\Traits\ConditionalTrait;
21
use Config\Feature;
22

23
/**
24
 * Class BaseBuilder
25
 *
26
 * Provides the core Query Builder methods.
27
 * Database-specific Builders might need to override
28
 * certain methods to make them work.
29
 */
30
class BaseBuilder
31
{
32
    use ConditionalTrait;
33

34
    /**
35
     * Reset DELETE data flag
36
     *
37
     * @var bool
38
     */
39
    protected $resetDeleteData = false;
40

41
    /**
42
     * QB SELECT data
43
     *
44
     * @var array
45
     */
46
    protected $QBSelect = [];
47

48
    /**
49
     * QB DISTINCT flag
50
     *
51
     * @var bool
52
     */
53
    protected $QBDistinct = false;
54

55
    /**
56
     * QB FROM data
57
     *
58
     * @var array
59
     */
60
    protected $QBFrom = [];
61

62
    /**
63
     * QB JOIN data
64
     *
65
     * @var array
66
     */
67
    protected $QBJoin = [];
68

69
    /**
70
     * QB WHERE data
71
     *
72
     * @var array
73
     */
74
    protected $QBWhere = [];
75

76
    /**
77
     * QB GROUP BY data
78
     *
79
     * @var array
80
     */
81
    public $QBGroupBy = [];
82

83
    /**
84
     * QB HAVING data
85
     *
86
     * @var array
87
     */
88
    protected $QBHaving = [];
89

90
    /**
91
     * QB keys
92
     * list of column names.
93
     *
94
     * @var list<string>
95
     */
96
    protected $QBKeys = [];
97

98
    /**
99
     * QB LIMIT data
100
     *
101
     * @var bool|int
102
     */
103
    protected $QBLimit = false;
104

105
    /**
106
     * QB OFFSET data
107
     *
108
     * @var bool|int
109
     */
110
    protected $QBOffset = false;
111

112
    /**
113
     * QB ORDER BY data
114
     *
115
     * @var array|string|null
116
     */
117
    public $QBOrderBy = [];
118

119
    /**
120
     * QB UNION data
121
     *
122
     * @var list<string>
123
     */
124
    protected array $QBUnion = [];
125

126
    /**
127
     * Whether to protect identifiers in SELECT
128
     *
129
     * @var list<bool|null> true=protect, false=not protect
130
     */
131
    public $QBNoEscape = [];
132

133
    /**
134
     * QB data sets
135
     *
136
     * @var array<string, string>|list<list<int|string>>
137
     */
138
    protected $QBSet = [];
139

140
    /**
141
     * QB WHERE group started flag
142
     *
143
     * @var bool
144
     */
145
    protected $QBWhereGroupStarted = false;
146

147
    /**
148
     * QB WHERE group count
149
     *
150
     * @var int
151
     */
152
    protected $QBWhereGroupCount = 0;
153

154
    /**
155
     * Ignore data that cause certain
156
     * exceptions, for example in case of
157
     * duplicate keys.
158
     *
159
     * @var bool
160
     */
161
    protected $QBIgnore = false;
162

163
    /**
164
     * QB Options data
165
     * Holds additional options and data used to render SQL
166
     * and is reset by resetWrite()
167
     *
168
     * @var array{
169
     *   updateFieldsAdditional?: array,
170
     *   tableIdentity?: string,
171
     *   updateFields?: array,
172
     *   constraints?: array,
173
     *   setQueryAsData?: string,
174
     *   sql?: string,
175
     *   alias?: string,
176
     *   fieldTypes?: array<string, array<string, string>>
177
     * }
178
     *
179
     * fieldTypes: [ProtectedTableName => [FieldName => Type]]
180
     */
181
    protected $QBOptions;
182

183
    /**
184
     * A reference to the database connection.
185
     *
186
     * @var BaseConnection
187
     */
188
    protected $db;
189

190
    /**
191
     * Name of the primary table for this instance.
192
     * Tracked separately because $QBFrom gets escaped
193
     * and prefixed.
194
     *
195
     * When $tableName to the constructor has multiple tables,
196
     * the value is empty string.
197
     *
198
     * @var string
199
     */
200
    protected $tableName;
201

202
    /**
203
     * ORDER BY random keyword
204
     *
205
     * @var array
206
     */
207
    protected $randomKeyword = [
208
        'RAND()',
209
        'RAND(%d)',
210
    ];
211

212
    /**
213
     * COUNT string
214
     *
215
     * @used-by CI_DB_driver::count_all()
216
     * @used-by BaseBuilder::count_all_results()
217
     *
218
     * @var string
219
     */
220
    protected $countString = 'SELECT COUNT(*) AS ';
221

222
    /**
223
     * Collects the named parameters and
224
     * their values for later binding
225
     * in the Query object.
226
     *
227
     * @var array
228
     */
229
    protected $binds = [];
230

231
    /**
232
     * Collects the key count for named parameters
233
     * in the Query object.
234
     *
235
     * @var array
236
     */
237
    protected $bindsKeyCount = [];
238

239
    /**
240
     * Some databases, like SQLite, do not by default
241
     * allow limiting of delete clauses.
242
     *
243
     * @var bool
244
     */
245
    protected $canLimitDeletes = true;
246

247
    /**
248
     * Some databases do not by default
249
     * allow limit update queries with WHERE.
250
     *
251
     * @var bool
252
     */
253
    protected $canLimitWhereUpdates = true;
254

255
    /**
256
     * Specifies which sql statements
257
     * support the ignore option.
258
     *
259
     * @var array
260
     */
261
    protected $supportedIgnoreStatements = [];
262

263
    /**
264
     * Builder testing mode status.
265
     *
266
     * @var bool
267
     */
268
    protected $testMode = false;
269

270
    /**
271
     * Tables relation types
272
     *
273
     * @var array
274
     */
275
    protected $joinTypes = [
276
        'LEFT',
277
        'RIGHT',
278
        'OUTER',
279
        'INNER',
280
        'LEFT OUTER',
281
        'RIGHT OUTER',
282
    ];
283

284
    /**
285
     * Strings that determine if a string represents a literal value or a field name
286
     *
287
     * @var list<string>
288
     */
289
    protected $isLiteralStr = [];
290

291
    /**
292
     * RegExp used to get operators
293
     *
294
     * @var list<string>
295
     */
296
    protected $pregOperators = [];
297

298
    /**
299
     * Constructor
300
     *
301
     * @param array|string|TableName $tableName tablename or tablenames with or without aliases
302
     *
303
     * Examples of $tableName: `mytable`, `jobs j`, `jobs j, users u`, `['jobs j','users u']`
304
     *
305
     * @throws DatabaseException
306
     */
307
    public function __construct($tableName, ConnectionInterface $db, ?array $options = null)
308
    {
309
        if (empty($tableName)) {
933✔
310
            throw new DatabaseException('A table must be specified when creating a new Query Builder.');
×
311
        }
312

313
        /**
314
         * @var BaseConnection $db
315
         */
316
        $this->db = $db;
933✔
317

318
        if ($tableName instanceof TableName) {
933✔
319
            $this->tableName = $tableName->getTableName();
7✔
320
            $this->QBFrom[]  = $this->db->escapeIdentifier($tableName);
7✔
321
            $this->db->addTableAlias($tableName->getAlias());
7✔
322
        }
323
        // If it contains `,`, it has multiple tables
324
        elseif (is_string($tableName) && ! str_contains($tableName, ',')) {
929✔
325
            $this->tableName = $tableName;  // @TODO remove alias if exists
927✔
326
            $this->from($tableName);
927✔
327
        } else {
328
            $this->tableName = '';
16✔
329
            $this->from($tableName);
16✔
330
        }
331

332
        if ($options !== null && $options !== []) {
933✔
333
            foreach ($options as $key => $value) {
×
334
                if (property_exists($this, $key)) {
×
335
                    $this->{$key} = $value;
×
336
                }
337
            }
338
        }
339
    }
340

341
    /**
342
     * Returns the current database connection
343
     *
344
     * @return BaseConnection
345
     */
346
    public function db(): ConnectionInterface
347
    {
348
        return $this->db;
1✔
349
    }
350

351
    /**
352
     * Sets a test mode status.
353
     *
354
     * @return $this
355
     */
356
    public function testMode(bool $mode = true)
357
    {
358
        $this->testMode = $mode;
77✔
359

360
        return $this;
77✔
361
    }
362

363
    /**
364
     * Gets the name of the primary table.
365
     */
366
    public function getTable(): string
367
    {
368
        return $this->tableName;
5✔
369
    }
370

371
    /**
372
     * Returns an array of bind values and their
373
     * named parameters for binding in the Query object later.
374
     */
375
    public function getBinds(): array
376
    {
377
        return $this->binds;
44✔
378
    }
379

380
    /**
381
     * Ignore
382
     *
383
     * Set ignore Flag for next insert,
384
     * update or delete query.
385
     *
386
     * @return $this
387
     */
388
    public function ignore(bool $ignore = true)
389
    {
390
        $this->QBIgnore = $ignore;
1✔
391

392
        return $this;
1✔
393
    }
394

395
    /**
396
     * Generates the SELECT portion of the query
397
     *
398
     * @param list<RawSql|string>|RawSql|string $select
399
     * @param bool|null                         $escape Whether to protect identifiers
400
     *
401
     * @return $this
402
     */
403
    public function select($select = '*', ?bool $escape = null)
404
    {
405
        // If the escape value was not set, we will base it on the global setting
406
        if (! is_bool($escape)) {
719✔
407
            $escape = $this->db->protectIdentifiers;
716✔
408
        }
409

410
        if ($select instanceof RawSql) {
719✔
411
            $select = [$select];
1✔
412
        }
413

414
        if (is_string($select)) {
719✔
415
            $select = ($escape === false) ? [$select] : explode(',', $select);
712✔
416
        }
417

418
        foreach ($select as $val) {
719✔
419
            if ($val instanceof RawSql) {
719✔
420
                $this->QBSelect[]   = $val;
5✔
421
                $this->QBNoEscape[] = false;
5✔
422

423
                continue;
5✔
424
            }
425

426
            $val = trim($val);
717✔
427

428
            if ($val !== '') {
717✔
429
                $this->QBSelect[] = $val;
717✔
430

431
                /*
432
                 * When doing 'SELECT NULL as field_alias FROM table'
433
                 * null gets taken as a field, and therefore escaped
434
                 * with backticks.
435
                 * This prevents NULL being escaped
436
                 * @see https://github.com/codeigniter4/CodeIgniter4/issues/1169
437
                 */
438
                if (mb_stripos($val, 'NULL') === 0) {
717✔
439
                    $this->QBNoEscape[] = false;
2✔
440

441
                    continue;
2✔
442
                }
443

444
                $this->QBNoEscape[] = $escape;
717✔
445
            }
446
        }
447

448
        return $this;
719✔
449
    }
450

451
    /**
452
     * Generates a SELECT MAX(field) portion of a query
453
     *
454
     * @return $this
455
     */
456
    public function selectMax(string $select = '', string $alias = '')
457
    {
458
        return $this->maxMinAvgSum($select, $alias);
654✔
459
    }
460

461
    /**
462
     * Generates a SELECT MIN(field) portion of a query
463
     *
464
     * @return $this
465
     */
466
    public function selectMin(string $select = '', string $alias = '')
467
    {
468
        return $this->maxMinAvgSum($select, $alias, 'MIN');
4✔
469
    }
470

471
    /**
472
     * Generates a SELECT AVG(field) portion of a query
473
     *
474
     * @return $this
475
     */
476
    public function selectAvg(string $select = '', string $alias = '')
477
    {
478
        return $this->maxMinAvgSum($select, $alias, 'AVG');
4✔
479
    }
480

481
    /**
482
     * Generates a SELECT SUM(field) portion of a query
483
     *
484
     * @return $this
485
     */
486
    public function selectSum(string $select = '', string $alias = '')
487
    {
488
        return $this->maxMinAvgSum($select, $alias, 'SUM');
6✔
489
    }
490

491
    /**
492
     * Generates a SELECT COUNT(field) portion of a query
493
     *
494
     * @return $this
495
     */
496
    public function selectCount(string $select = '', string $alias = '')
497
    {
498
        return $this->maxMinAvgSum($select, $alias, 'COUNT');
5✔
499
    }
500

501
    /**
502
     * Adds a subquery to the selection
503
     */
504
    public function selectSubquery(BaseBuilder $subquery, string $as): self
505
    {
506
        $this->QBSelect[] = $this->buildSubquery($subquery, true, $as);
2✔
507

508
        return $this;
2✔
509
    }
510

511
    /**
512
     * SELECT [MAX|MIN|AVG|SUM|COUNT]()
513
     *
514
     * @used-by selectMax()
515
     * @used-by selectMin()
516
     * @used-by selectAvg()
517
     * @used-by selectSum()
518
     *
519
     * @return $this
520
     *
521
     * @throws DatabaseException
522
     * @throws DataException
523
     */
524
    protected function maxMinAvgSum(string $select = '', string $alias = '', string $type = 'MAX')
525
    {
526
        if ($select === '') {
664✔
527
            throw DataException::forEmptyInputGiven('Select');
1✔
528
        }
529

530
        if (str_contains($select, ',')) {
663✔
531
            throw DataException::forInvalidArgument('column name not separated by comma');
1✔
532
        }
533

534
        $type = strtoupper($type);
662✔
535

536
        if (! in_array($type, ['MAX', 'MIN', 'AVG', 'SUM', 'COUNT'], true)) {
662✔
537
            throw new DatabaseException('Invalid function type: ' . $type);
×
538
        }
539

540
        if ($alias === '') {
662✔
541
            $alias = $this->createAliasFromTable(trim($select));
657✔
542
        }
543

544
        $sql = $type . '(' . $this->db->protectIdentifiers(trim($select)) . ') AS ' . $this->db->escapeIdentifiers(trim($alias));
662✔
545

546
        $this->QBSelect[]   = $sql;
662✔
547
        $this->QBNoEscape[] = null;
662✔
548

549
        return $this;
662✔
550
    }
551

552
    /**
553
     * Determines the alias name based on the table
554
     */
555
    protected function createAliasFromTable(string $item): string
556
    {
557
        if (str_contains($item, '.')) {
657✔
558
            $item = explode('.', $item);
1✔
559

560
            return end($item);
1✔
561
        }
562

563
        return $item;
656✔
564
    }
565

566
    /**
567
     * Sets a flag which tells the query string compiler to add DISTINCT
568
     *
569
     * @return $this
570
     */
571
    public function distinct(bool $val = true)
572
    {
573
        $this->QBDistinct = $val;
656✔
574

575
        return $this;
656✔
576
    }
577

578
    /**
579
     * Generates the FROM portion of the query
580
     *
581
     * @param array|string $from
582
     *
583
     * @return $this
584
     */
585
    public function from($from, bool $overwrite = false): self
586
    {
587
        if ($overwrite) {
933✔
588
            $this->QBFrom = [];
871✔
589
            $this->db->setAliasedTables([]);
871✔
590
        }
591

592
        foreach ((array) $from as $table) {
933✔
593
            if (str_contains($table, ',')) {
933✔
594
                $this->from(explode(',', $table));
17✔
595
            } else {
596
                $table = trim($table);
933✔
597

598
                if ($table === '') {
933✔
599
                    continue;
14✔
600
                }
601

602
                $this->trackAliases($table);
933✔
603
                $this->QBFrom[] = $this->db->protectIdentifiers($table, true, null, false);
933✔
604
            }
605
        }
606

607
        return $this;
933✔
608
    }
609

610
    /**
611
     * @param BaseBuilder $from  Expected subquery
612
     * @param string      $alias Subquery alias
613
     *
614
     * @return $this
615
     */
616
    public function fromSubquery(BaseBuilder $from, string $alias): self
617
    {
618
        $table = $this->buildSubquery($from, true, $alias);
5✔
619

620
        $this->db->addTableAlias($alias);
4✔
621
        $this->QBFrom[] = $table;
4✔
622

623
        return $this;
4✔
624
    }
625

626
    /**
627
     * Generates the JOIN portion of the query
628
     *
629
     * @param RawSql|string $cond
630
     *
631
     * @return $this
632
     */
633
    public function join(string $table, $cond, string $type = '', ?bool $escape = null)
634
    {
635
        if ($type !== '') {
15✔
636
            $type = strtoupper(trim($type));
8✔
637

638
            if (! in_array($type, $this->joinTypes, true)) {
8✔
639
                $type = '';
×
640
            } else {
641
                $type .= ' ';
8✔
642
            }
643
        }
644

645
        // Extract any aliases that might exist. We use this information
646
        // in the protectIdentifiers to know whether to add a table prefix
647
        $this->trackAliases($table);
15✔
648

649
        if (! is_bool($escape)) {
15✔
650
            $escape = $this->db->protectIdentifiers;
15✔
651
        }
652

653
        // Do we want to escape the table name?
654
        if ($escape === true) {
15✔
655
            $table = $this->db->protectIdentifiers($table, true, null, false);
15✔
656
        }
657

658
        if ($cond instanceof RawSql) {
15✔
659
            $this->QBJoin[] = $type . 'JOIN ' . $table . ' ON ' . $cond;
1✔
660

661
            return $this;
1✔
662
        }
663

664
        if (! $this->hasOperator($cond)) {
14✔
665
            $cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')';
×
666
        } elseif ($escape === false) {
14✔
667
            $cond = ' ON ' . $cond;
×
668
        } else {
669
            // Split multiple conditions
670
            // @TODO This does not parse `BETWEEN a AND b` correctly.
671
            if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE) >= 1) {
14✔
672
                $conditions = [];
2✔
673
                $joints     = $joints[0];
2✔
674
                array_unshift($joints, ['', 0]);
2✔
675

676
                for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--) {
2✔
677
                    $joints[$i][1] += strlen($joints[$i][0]); // offset
2✔
678
                    $conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]);
2✔
679
                    $pos            = $joints[$i][1] - strlen($joints[$i][0]);
2✔
680
                    $joints[$i]     = $joints[$i][0];
2✔
681
                }
682
                ksort($conditions);
2✔
683
            } else {
684
                $conditions = [$cond];
12✔
685
                $joints     = [''];
12✔
686
            }
687

688
            $cond = ' ON ';
14✔
689

690
            foreach ($conditions as $i => $condition) {
14✔
691
                $operator = $this->getOperator($condition);
14✔
692

693
                // Workaround for BETWEEN
694
                if ($operator === false) {
14✔
695
                    $cond .= $joints[$i] . $condition;
1✔
696

697
                    continue;
1✔
698
                }
699

700
                $cond .= $joints[$i];
14✔
701
                $cond .= preg_match('/(\(*)?([\[\]\w\.\'-]+)' . preg_quote($operator, '/') . '(.*)/i', $condition, $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $condition;
14✔
702
            }
703
        }
704

705
        // Assemble the JOIN statement
706
        $this->QBJoin[] = $type . 'JOIN ' . $table . $cond;
14✔
707

708
        return $this;
14✔
709
    }
710

711
    /**
712
     * Generates the WHERE portion of the query.
713
     * Separates multiple calls with 'AND'.
714
     *
715
     * @param array|RawSql|string $key
716
     * @param mixed               $value
717
     *
718
     * @return $this
719
     */
720
    public function where($key, $value = null, ?bool $escape = null)
721
    {
722
        return $this->whereHaving('QBWhere', $key, $value, 'AND ', $escape);
764✔
723
    }
724

725
    /**
726
     * OR WHERE
727
     *
728
     * Generates the WHERE portion of the query.
729
     * Separates multiple calls with 'OR'.
730
     *
731
     * @param array|RawSql|string $key
732
     * @param mixed               $value
733
     *
734
     * @return $this
735
     */
736
    public function orWhere($key, $value = null, ?bool $escape = null)
737
    {
738
        return $this->whereHaving('QBWhere', $key, $value, 'OR ', $escape);
5✔
739
    }
740

741
    /**
742
     * @used-by where()
743
     * @used-by orWhere()
744
     * @used-by having()
745
     * @used-by orHaving()
746
     *
747
     * @param array|RawSql|string $key
748
     * @param mixed               $value
749
     *
750
     * @return $this
751
     */
752
    protected function whereHaving(string $qbKey, $key, $value = null, string $type = 'AND ', ?bool $escape = null)
753
    {
754
        $rawSqlOnly = false;
770✔
755

756
        if ($key instanceof RawSql) {
770✔
757
            if ($value === null) {
4✔
758
                $keyValue   = [(string) $key => $key];
1✔
759
                $rawSqlOnly = true;
1✔
760
            } else {
761
                $keyValue = [(string) $key => $value];
3✔
762
            }
763
        } elseif (! is_array($key)) {
766✔
764
            $keyValue = [$key => $value];
756✔
765
        } else {
766
            $keyValue = $key;
171✔
767
        }
768

769
        // If the escape value was not set will base it on the global setting
770
        if (! is_bool($escape)) {
770✔
771
            $escape = $this->db->protectIdentifiers;
766✔
772
        }
773

774
        foreach ($keyValue as $k => $v) {
770✔
775
            $prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type);
770✔
776

777
            if ($rawSqlOnly) {
770✔
778
                $k  = '';
1✔
779
                $op = '';
1✔
780
            } elseif ($v !== null) {
769✔
781
                $op = $this->getOperatorFromWhereKey($k);
749✔
782

783
                if (! empty($op)) {
749✔
784
                    $k = trim($k);
49✔
785

786
                    end($op);
49✔
787
                    $op = trim(current($op));
49✔
788

789
                    // Does the key end with operator?
790
                    if (str_ends_with($k, $op)) {
49✔
791
                        $k  = rtrim(substr($k, 0, -strlen($op)));
49✔
792
                        $op = " {$op}";
49✔
793
                    } else {
794
                        $op = '';
×
795
                    }
796
                } else {
797
                    $op = ' =';
737✔
798
                }
799

800
                if ($this->isSubquery($v)) {
749✔
801
                    $v = $this->buildSubquery($v, true);
1✔
802
                } else {
803
                    $bind = $this->setBind($k, $v, $escape);
749✔
804
                    $v    = " :{$bind}:";
749✔
805
                }
806
            } elseif (! $this->hasOperator($k) && $qbKey !== 'QBHaving') {
113✔
807
                // value appears not to have been set, assign the test to IS NULL
808
                $op = ' IS NULL';
80✔
809
            } elseif (
810
                // The key ends with !=, =, <>, IS, IS NOT
811
                preg_match(
39✔
812
                    '/\s*(!?=|<>|IS(?:\s+NOT)?)\s*$/i',
39✔
813
                    $k,
39✔
814
                    $match,
39✔
815
                    PREG_OFFSET_CAPTURE
39✔
816
                )
39✔
817
            ) {
818
                $k  = substr($k, 0, $match[0][1]);
1✔
819
                $op = $match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL';
1✔
820
            } else {
821
                $op = '';
38✔
822
            }
823

824
            if ($v instanceof RawSql) {
770✔
825
                $this->{$qbKey}[] = [
1✔
826
                    'condition' => $v->with($prefix . $k . $op . $v),
1✔
827
                    'escape'    => $escape,
1✔
828
                ];
1✔
829
            } else {
830
                $this->{$qbKey}[] = [
769✔
831
                    'condition' => $prefix . $k . $op . $v,
769✔
832
                    'escape'    => $escape,
769✔
833
                ];
769✔
834
            }
835
        }
836

837
        return $this;
770✔
838
    }
839

840
    /**
841
     * Generates a WHERE field IN('item', 'item') SQL query,
842
     * joined with 'AND' if appropriate.
843
     *
844
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
845
     *
846
     * @return $this
847
     */
848
    public function whereIn(?string $key = null, $values = null, ?bool $escape = null)
849
    {
850
        return $this->_whereIn($key, $values, false, 'AND ', $escape);
64✔
851
    }
852

853
    /**
854
     * Generates a WHERE field IN('item', 'item') SQL query,
855
     * joined with 'OR' if appropriate.
856
     *
857
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
858
     *
859
     * @return $this
860
     */
861
    public function orWhereIn(?string $key = null, $values = null, ?bool $escape = null)
862
    {
863
        return $this->_whereIn($key, $values, false, 'OR ', $escape);
3✔
864
    }
865

866
    /**
867
     * Generates a WHERE field NOT IN('item', 'item') SQL query,
868
     * joined with 'AND' if appropriate.
869
     *
870
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
871
     *
872
     * @return $this
873
     */
874
    public function whereNotIn(?string $key = null, $values = null, ?bool $escape = null)
875
    {
876
        return $this->_whereIn($key, $values, true, 'AND ', $escape);
3✔
877
    }
878

879
    /**
880
     * Generates a WHERE field NOT IN('item', 'item') SQL query,
881
     * joined with 'OR' if appropriate.
882
     *
883
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
884
     *
885
     * @return $this
886
     */
887
    public function orWhereNotIn(?string $key = null, $values = null, ?bool $escape = null)
888
    {
889
        return $this->_whereIn($key, $values, true, 'OR ', $escape);
2✔
890
    }
891

892
    /**
893
     * Generates a HAVING field IN('item', 'item') SQL query,
894
     * joined with 'AND' if appropriate.
895
     *
896
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
897
     *
898
     * @return $this
899
     */
900
    public function havingIn(?string $key = null, $values = null, ?bool $escape = null)
901
    {
902
        return $this->_whereIn($key, $values, false, 'AND ', $escape, 'QBHaving');
6✔
903
    }
904

905
    /**
906
     * Generates a HAVING field IN('item', 'item') SQL query,
907
     * joined with 'OR' if appropriate.
908
     *
909
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
910
     *
911
     * @return $this
912
     */
913
    public function orHavingIn(?string $key = null, $values = null, ?bool $escape = null)
914
    {
915
        return $this->_whereIn($key, $values, false, 'OR ', $escape, 'QBHaving');
3✔
916
    }
917

918
    /**
919
     * Generates a HAVING field NOT IN('item', 'item') SQL query,
920
     * joined with 'AND' if appropriate.
921
     *
922
     * @param array|BaseBuilder|(Closure(BaseBuilder):BaseBuilder)|null $values The values searched on, or anonymous function with subquery
923
     *
924
     * @return $this
925
     */
926
    public function havingNotIn(?string $key = null, $values = null, ?bool $escape = null)
927
    {
928
        return $this->_whereIn($key, $values, true, 'AND ', $escape, 'QBHaving');
5✔
929
    }
930

931
    /**
932
     * Generates a HAVING field NOT IN('item', 'item') SQL query,
933
     * joined with 'OR' if appropriate.
934
     *
935
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
936
     *
937
     * @return $this
938
     */
939
    public function orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null)
940
    {
941
        return $this->_whereIn($key, $values, true, 'OR ', $escape, 'QBHaving');
3✔
942
    }
943

944
    /**
945
     * @used-by WhereIn()
946
     * @used-by orWhereIn()
947
     * @used-by whereNotIn()
948
     * @used-by orWhereNotIn()
949
     *
950
     * @param non-empty-string|null                                      $key
951
     * @param array|BaseBuilder|(Closure(BaseBuilder): BaseBuilder)|null $values The values searched on, or anonymous function with subquery
952
     *
953
     * @return $this
954
     *
955
     * @throws InvalidArgumentException
956
     */
957
    protected function _whereIn(?string $key = null, $values = null, bool $not = false, string $type = 'AND ', ?bool $escape = null, string $clause = 'QBWhere')
958
    {
959
        if ($key === null || $key === '') {
84✔
960
            throw new InvalidArgumentException(sprintf('%s() expects $key to be a non-empty string', debug_backtrace(0, 2)[1]['function']));
2✔
961
        }
962

963
        if ($values === null || (! is_array($values) && ! $this->isSubquery($values))) {
82✔
964
            throw new InvalidArgumentException(sprintf('%s() expects $values to be of type array or closure', debug_backtrace(0, 2)[1]['function']));
3✔
965
        }
966

967
        if (! is_bool($escape)) {
79✔
968
            $escape = $this->db->protectIdentifiers;
79✔
969
        }
970

971
        $ok = $key;
79✔
972

973
        if ($escape === true) {
79✔
974
            $key = $this->db->protectIdentifiers($key);
79✔
975
        }
976

977
        $not = ($not) ? ' NOT' : '';
79✔
978

979
        if ($this->isSubquery($values)) {
79✔
980
            $whereIn = $this->buildSubquery($values, true);
8✔
981
            $escape  = false;
8✔
982
        } else {
983
            $whereIn = array_values($values);
71✔
984
        }
985

986
        $ok = $this->setBind($ok, $whereIn, $escape);
79✔
987

988
        $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
79✔
989

990
        $whereIn = [
79✔
991
            'condition' => "{$prefix}{$key}{$not} IN :{$ok}:",
79✔
992
            'escape'    => false,
79✔
993
        ];
79✔
994

995
        $this->{$clause}[] = $whereIn;
79✔
996

997
        return $this;
79✔
998
    }
999

1000
    /**
1001
     * Generates a %LIKE% portion of the query.
1002
     * Separates multiple calls with 'AND'.
1003
     *
1004
     * @param array|RawSql|string $field
1005
     *
1006
     * @return $this
1007
     */
1008
    public function like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1009
    {
1010
        return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch);
25✔
1011
    }
1012

1013
    /**
1014
     * Generates a NOT LIKE portion of the query.
1015
     * Separates multiple calls with 'AND'.
1016
     *
1017
     * @param array|RawSql|string $field
1018
     *
1019
     * @return $this
1020
     */
1021
    public function notLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1022
    {
1023
        return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch);
2✔
1024
    }
1025

1026
    /**
1027
     * Generates a %LIKE% portion of the query.
1028
     * Separates multiple calls with 'OR'.
1029
     *
1030
     * @param array|RawSql|string $field
1031
     *
1032
     * @return $this
1033
     */
1034
    public function orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1035
    {
1036
        return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch);
2✔
1037
    }
1038

1039
    /**
1040
     * Generates a NOT LIKE portion of the query.
1041
     * Separates multiple calls with 'OR'.
1042
     *
1043
     * @param array|RawSql|string $field
1044
     *
1045
     * @return $this
1046
     */
1047
    public function orNotLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1048
    {
1049
        return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape, $insensitiveSearch);
2✔
1050
    }
1051

1052
    /**
1053
     * Generates a %LIKE% portion of the query.
1054
     * Separates multiple calls with 'AND'.
1055
     *
1056
     * @param array|RawSql|string $field
1057
     *
1058
     * @return $this
1059
     */
1060
    public function havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1061
    {
1062
        return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch, 'QBHaving');
15✔
1063
    }
1064

1065
    /**
1066
     * Generates a NOT LIKE portion of the query.
1067
     * Separates multiple calls with 'AND'.
1068
     *
1069
     * @param array|RawSql|string $field
1070
     *
1071
     * @return $this
1072
     */
1073
    public function notHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1074
    {
1075
        return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch, 'QBHaving');
4✔
1076
    }
1077

1078
    /**
1079
     * Generates a %LIKE% portion of the query.
1080
     * Separates multiple calls with 'OR'.
1081
     *
1082
     * @param array|RawSql|string $field
1083
     *
1084
     * @return $this
1085
     */
1086
    public function orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1087
    {
1088
        return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch, 'QBHaving');
4✔
1089
    }
1090

1091
    /**
1092
     * Generates a NOT LIKE portion of the query.
1093
     * Separates multiple calls with 'OR'.
1094
     *
1095
     * @param array|RawSql|string $field
1096
     *
1097
     * @return $this
1098
     */
1099
    public function orNotHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1100
    {
1101
        return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape, $insensitiveSearch, 'QBHaving');
4✔
1102
    }
1103

1104
    /**
1105
     * @used-by like()
1106
     * @used-by orLike()
1107
     * @used-by notLike()
1108
     * @used-by orNotLike()
1109
     * @used-by havingLike()
1110
     * @used-by orHavingLike()
1111
     * @used-by notHavingLike()
1112
     * @used-by orNotHavingLike()
1113
     *
1114
     * @param array<string, string>|RawSql|string $field
1115
     *
1116
     * @return $this
1117
     */
1118
    protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', ?bool $escape = null, bool $insensitiveSearch = false, string $clause = 'QBWhere')
1119
    {
1120
        $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
47✔
1121
        $side   = strtolower($side);
47✔
1122

1123
        if ($field instanceof RawSql) {
47✔
1124
            $k                 = (string) $field;
3✔
1125
            $v                 = $match;
3✔
1126
            $insensitiveSearch = false;
3✔
1127

1128
            $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
3✔
1129

1130
            if ($side === 'none') {
3✔
1131
                $bind = $this->setBind($field->getBindingKey(), $v, $escape);
×
1132
            } elseif ($side === 'before') {
3✔
1133
                $bind = $this->setBind($field->getBindingKey(), "%{$v}", $escape);
×
1134
            } elseif ($side === 'after') {
3✔
1135
                $bind = $this->setBind($field->getBindingKey(), "{$v}%", $escape);
×
1136
            } else {
1137
                $bind = $this->setBind($field->getBindingKey(), "%{$v}%", $escape);
3✔
1138
            }
1139

1140
            $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
3✔
1141

1142
            // some platforms require an escape sequence definition for LIKE wildcards
1143
            if ($escape === true && $this->db->likeEscapeStr !== '') {
3✔
1144
                $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar);
3✔
1145
            }
1146

1147
            $this->{$clause}[] = [
3✔
1148
                'condition' => $field->with($likeStatement),
3✔
1149
                'escape'    => $escape,
3✔
1150
            ];
3✔
1151

1152
            return $this;
3✔
1153
        }
1154

1155
        $keyValue = ! is_array($field) ? [$field => $match] : $field;
44✔
1156

1157
        foreach ($keyValue as $k => $v) {
44✔
1158
            if ($insensitiveSearch) {
44✔
1159
                $v = mb_strtolower($v, 'UTF-8');
7✔
1160
            }
1161

1162
            $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
44✔
1163

1164
            if ($side === 'none') {
44✔
1165
                $bind = $this->setBind($k, $v, $escape);
1✔
1166
            } elseif ($side === 'before') {
43✔
1167
                $bind = $this->setBind($k, "%{$v}", $escape);
9✔
1168
            } elseif ($side === 'after') {
34✔
1169
                $bind = $this->setBind($k, "{$v}%", $escape);
5✔
1170
            } else {
1171
                $bind = $this->setBind($k, "%{$v}%", $escape);
29✔
1172
            }
1173

1174
            $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
44✔
1175

1176
            // some platforms require an escape sequence definition for LIKE wildcards
1177
            if ($escape === true && $this->db->likeEscapeStr !== '') {
44✔
1178
                $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar);
44✔
1179
            }
1180

1181
            $this->{$clause}[] = [
44✔
1182
                'condition' => $likeStatement,
44✔
1183
                'escape'    => $escape,
44✔
1184
            ];
44✔
1185
        }
1186

1187
        return $this;
44✔
1188
    }
1189

1190
    /**
1191
     * Platform independent LIKE statement builder.
1192
     */
1193
    protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string
1194
    {
1195
        if ($insensitiveSearch) {
47✔
1196
            return "{$prefix} LOWER(" . $this->db->escapeIdentifiers($column) . ") {$not} LIKE :{$bind}:";
7✔
1197
        }
1198

1199
        return "{$prefix} {$column} {$not} LIKE :{$bind}:";
40✔
1200
    }
1201

1202
    /**
1203
     * Add UNION statement
1204
     *
1205
     * @param BaseBuilder|Closure(BaseBuilder): BaseBuilder $union
1206
     *
1207
     * @return $this
1208
     */
1209
    public function union($union)
1210
    {
1211
        return $this->addUnionStatement($union);
4✔
1212
    }
1213

1214
    /**
1215
     * Add UNION ALL statement
1216
     *
1217
     * @param BaseBuilder|Closure(BaseBuilder): BaseBuilder $union
1218
     *
1219
     * @return $this
1220
     */
1221
    public function unionAll($union)
1222
    {
1223
        return $this->addUnionStatement($union, true);
2✔
1224
    }
1225

1226
    /**
1227
     * @used-by union()
1228
     * @used-by unionAll()
1229
     *
1230
     * @param BaseBuilder|Closure(BaseBuilder): BaseBuilder $union
1231
     *
1232
     * @return $this
1233
     */
1234
    protected function addUnionStatement($union, bool $all = false)
1235
    {
1236
        $this->QBUnion[] = "\nUNION "
6✔
1237
            . ($all ? 'ALL ' : '')
6✔
1238
            . 'SELECT * FROM '
6✔
1239
            . $this->buildSubquery($union, true, 'uwrp' . (count($this->QBUnion) + 1));
6✔
1240

1241
        return $this;
6✔
1242
    }
1243

1244
    /**
1245
     * Starts a query group.
1246
     *
1247
     * @return $this
1248
     */
1249
    public function groupStart()
1250
    {
1251
        return $this->groupStartPrepare();
2✔
1252
    }
1253

1254
    /**
1255
     * Starts a query group, but ORs the group
1256
     *
1257
     * @return $this
1258
     */
1259
    public function orGroupStart()
1260
    {
1261
        return $this->groupStartPrepare('', 'OR ');
2✔
1262
    }
1263

1264
    /**
1265
     * Starts a query group, but NOTs the group
1266
     *
1267
     * @return $this
1268
     */
1269
    public function notGroupStart()
1270
    {
1271
        return $this->groupStartPrepare('NOT ');
2✔
1272
    }
1273

1274
    /**
1275
     * Starts a query group, but OR NOTs the group
1276
     *
1277
     * @return $this
1278
     */
1279
    public function orNotGroupStart()
1280
    {
1281
        return $this->groupStartPrepare('NOT ', 'OR ');
2✔
1282
    }
1283

1284
    /**
1285
     * Ends a query group
1286
     *
1287
     * @return $this
1288
     */
1289
    public function groupEnd()
1290
    {
1291
        return $this->groupEndPrepare();
8✔
1292
    }
1293

1294
    /**
1295
     * Starts a query group for HAVING clause.
1296
     *
1297
     * @return $this
1298
     */
1299
    public function havingGroupStart()
1300
    {
1301
        return $this->groupStartPrepare('', 'AND ', 'QBHaving');
2✔
1302
    }
1303

1304
    /**
1305
     * Starts a query group for HAVING clause, but ORs the group.
1306
     *
1307
     * @return $this
1308
     */
1309
    public function orHavingGroupStart()
1310
    {
1311
        return $this->groupStartPrepare('', 'OR ', 'QBHaving');
2✔
1312
    }
1313

1314
    /**
1315
     * Starts a query group for HAVING clause, but NOTs the group.
1316
     *
1317
     * @return $this
1318
     */
1319
    public function notHavingGroupStart()
1320
    {
1321
        return $this->groupStartPrepare('NOT ', 'AND ', 'QBHaving');
2✔
1322
    }
1323

1324
    /**
1325
     * Starts a query group for HAVING clause, but OR NOTs the group.
1326
     *
1327
     * @return $this
1328
     */
1329
    public function orNotHavingGroupStart()
1330
    {
1331
        return $this->groupStartPrepare('NOT ', 'OR ', 'QBHaving');
2✔
1332
    }
1333

1334
    /**
1335
     * Ends a query group for HAVING clause.
1336
     *
1337
     * @return $this
1338
     */
1339
    public function havingGroupEnd()
1340
    {
1341
        return $this->groupEndPrepare('QBHaving');
8✔
1342
    }
1343

1344
    /**
1345
     * Prepate a query group start.
1346
     *
1347
     * @return $this
1348
     */
1349
    protected function groupStartPrepare(string $not = '', string $type = 'AND ', string $clause = 'QBWhere')
1350
    {
1351
        $type = $this->groupGetType($type);
16✔
1352

1353
        $this->QBWhereGroupStarted = true;
16✔
1354
        $prefix                    = empty($this->{$clause}) ? '' : $type;
16✔
1355
        $where                     = [
16✔
1356
            'condition' => $prefix . $not . str_repeat(' ', ++$this->QBWhereGroupCount) . ' (',
16✔
1357
            'escape'    => false,
16✔
1358
        ];
16✔
1359

1360
        $this->{$clause}[] = $where;
16✔
1361

1362
        return $this;
16✔
1363
    }
1364

1365
    /**
1366
     * Prepate a query group end.
1367
     *
1368
     * @return $this
1369
     */
1370
    protected function groupEndPrepare(string $clause = 'QBWhere')
1371
    {
1372
        $this->QBWhereGroupStarted = false;
16✔
1373
        $where                     = [
16✔
1374
            'condition' => str_repeat(' ', $this->QBWhereGroupCount--) . ')',
16✔
1375
            'escape'    => false,
16✔
1376
        ];
16✔
1377

1378
        $this->{$clause}[] = $where;
16✔
1379

1380
        return $this;
16✔
1381
    }
1382

1383
    /**
1384
     * @used-by groupStart()
1385
     * @used-by _like()
1386
     * @used-by whereHaving()
1387
     * @used-by _whereIn()
1388
     * @used-by havingGroupStart()
1389
     */
1390
    protected function groupGetType(string $type): string
1391
    {
1392
        if ($this->QBWhereGroupStarted) {
799✔
1393
            $type                      = '';
16✔
1394
            $this->QBWhereGroupStarted = false;
16✔
1395
        }
1396

1397
        return $type;
799✔
1398
    }
1399

1400
    /**
1401
     * @param array|string $by
1402
     *
1403
     * @return $this
1404
     */
1405
    public function groupBy($by, ?bool $escape = null)
1406
    {
1407
        if (! is_bool($escape)) {
56✔
1408
            $escape = $this->db->protectIdentifiers;
56✔
1409
        }
1410

1411
        if (is_string($by)) {
56✔
1412
            $by = ($escape === true) ? explode(',', $by) : [$by];
56✔
1413
        }
1414

1415
        foreach ($by as $val) {
56✔
1416
            $val = trim($val);
56✔
1417

1418
            if ($val !== '') {
56✔
1419
                $val = [
56✔
1420
                    'field'  => $val,
56✔
1421
                    'escape' => $escape,
56✔
1422
                ];
56✔
1423

1424
                $this->QBGroupBy[] = $val;
56✔
1425
            }
1426
        }
1427

1428
        return $this;
56✔
1429
    }
1430

1431
    /**
1432
     * Separates multiple calls with 'AND'.
1433
     *
1434
     * @param array|RawSql|string $key
1435
     * @param mixed               $value
1436
     *
1437
     * @return $this
1438
     */
1439
    public function having($key, $value = null, ?bool $escape = null)
1440
    {
1441
        return $this->whereHaving('QBHaving', $key, $value, 'AND ', $escape);
16✔
1442
    }
1443

1444
    /**
1445
     * Separates multiple calls with 'OR'.
1446
     *
1447
     * @param array|RawSql|string $key
1448
     * @param mixed               $value
1449
     *
1450
     * @return $this
1451
     */
1452
    public function orHaving($key, $value = null, ?bool $escape = null)
1453
    {
1454
        return $this->whereHaving('QBHaving', $key, $value, 'OR ', $escape);
2✔
1455
    }
1456

1457
    /**
1458
     * @param string $direction ASC, DESC or RANDOM
1459
     *
1460
     * @return $this
1461
     */
1462
    public function orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
1463
    {
1464
        if ($orderBy === '') {
677✔
1465
            return $this;
×
1466
        }
1467

1468
        $qbOrderBy = [];
677✔
1469

1470
        $direction = strtoupper(trim($direction));
677✔
1471

1472
        if ($direction === 'RANDOM') {
677✔
1473
            $direction = '';
3✔
1474
            $orderBy   = ctype_digit($orderBy) ? sprintf($this->randomKeyword[1], $orderBy) : $this->randomKeyword[0];
3✔
1475
            $escape    = false;
3✔
1476
        } elseif ($direction !== '') {
675✔
1477
            $direction = in_array($direction, ['ASC', 'DESC'], true) ? ' ' . $direction : '';
675✔
1478
        }
1479

1480
        if ($escape === null) {
677✔
1481
            $escape = $this->db->protectIdentifiers;
675✔
1482
        }
1483

1484
        if ($escape === false) {
677✔
1485
            $qbOrderBy[] = [
3✔
1486
                'field'     => $orderBy,
3✔
1487
                'direction' => $direction,
3✔
1488
                'escape'    => false,
3✔
1489
            ];
3✔
1490
        } else {
1491
            foreach (explode(',', $orderBy) as $field) {
675✔
1492
                $qbOrderBy[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE))
675✔
1493
                    ? [
×
1494
                        'field'     => ltrim(substr($field, 0, $match[0][1])),
×
1495
                        'direction' => ' ' . $match[1][0],
×
1496
                        'escape'    => true,
×
1497
                    ]
×
1498
                    : [
675✔
1499
                        'field'     => trim($field),
675✔
1500
                        'direction' => $direction,
675✔
1501
                        'escape'    => true,
675✔
1502
                    ];
675✔
1503
            }
1504
        }
1505

1506
        $this->QBOrderBy = array_merge($this->QBOrderBy, $qbOrderBy);
677✔
1507

1508
        return $this;
677✔
1509
    }
1510

1511
    /**
1512
     * @return $this
1513
     */
1514
    public function limit(?int $value = null, ?int $offset = 0)
1515
    {
1516
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
105✔
1517
        if ($limitZeroAsAll && $value === 0) {
105✔
1518
            $value = null;
13✔
1519
        }
1520

1521
        if ($value !== null) {
105✔
1522
            $this->QBLimit = $value;
93✔
1523
        }
1524

1525
        if ($offset !== null && $offset !== 0) {
105✔
1526
            $this->QBOffset = $offset;
9✔
1527
        }
1528

1529
        return $this;
105✔
1530
    }
1531

1532
    /**
1533
     * Sets the OFFSET value
1534
     *
1535
     * @return $this
1536
     */
1537
    public function offset(int $offset)
1538
    {
1539
        if ($offset !== 0) {
1✔
1540
            $this->QBOffset = $offset;
1✔
1541
        }
1542

1543
        return $this;
1✔
1544
    }
1545

1546
    /**
1547
     * Generates a platform-specific LIMIT clause.
1548
     */
1549
    protected function _limit(string $sql, bool $offsetIgnore = false): string
1550
    {
1551
        return $sql . ' LIMIT ' . ($offsetIgnore === false && $this->QBOffset ? $this->QBOffset . ', ' : '') . $this->QBLimit;
94✔
1552
    }
1553

1554
    /**
1555
     * Allows key/value pairs to be set for insert(), update() or replace().
1556
     *
1557
     * @param array|object|string $key    Field name, or an array of field/value pairs, or an object
1558
     * @param mixed               $value  Field value, if $key is a single field
1559
     * @param bool|null           $escape Whether to escape values
1560
     *
1561
     * @return $this
1562
     */
1563
    public function set($key, $value = '', ?bool $escape = null)
1564
    {
1565
        $key = $this->objectToArray($key);
694✔
1566

1567
        if (! is_array($key)) {
694✔
1568
            $key = [$key => $value];
116✔
1569
        }
1570

1571
        $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
694✔
1572

1573
        foreach ($key as $k => $v) {
694✔
1574
            if ($escape) {
694✔
1575
                $bind = $this->setBind($k, $v, $escape);
693✔
1576

1577
                $this->QBSet[$this->db->protectIdentifiers($k, false)] = ":{$bind}:";
693✔
1578
            } else {
1579
                $this->QBSet[$this->db->protectIdentifiers($k, false)] = $v;
10✔
1580
            }
1581
        }
1582

1583
        return $this;
694✔
1584
    }
1585

1586
    /**
1587
     * Returns the previously set() data, alternatively resetting it if needed.
1588
     */
1589
    public function getSetData(bool $clean = false): array
1590
    {
1591
        $data = $this->QBSet;
×
1592

1593
        if ($clean) {
×
1594
            $this->QBSet = [];
×
1595
        }
1596

1597
        return $data;
×
1598
    }
1599

1600
    /**
1601
     * Compiles a SELECT query string and returns the sql.
1602
     */
1603
    public function getCompiledSelect(bool $reset = true): string
1604
    {
1605
        $select = $this->compileSelect();
176✔
1606

1607
        if ($reset) {
176✔
1608
            $this->resetSelect();
174✔
1609
        }
1610

1611
        return $this->compileFinalQuery($select);
176✔
1612
    }
1613

1614
    /**
1615
     * Returns a finalized, compiled query string with the bindings
1616
     * inserted and prefixes swapped out.
1617
     */
1618
    protected function compileFinalQuery(string $sql): string
1619
    {
1620
        $query = new Query($this->db);
201✔
1621
        $query->setQuery($sql, $this->binds, false);
201✔
1622

1623
        if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
201✔
1624
            $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
×
1625
        }
1626

1627
        return $query->getQuery();
201✔
1628
    }
1629

1630
    /**
1631
     * Compiles the select statement based on the other functions called
1632
     * and runs the query
1633
     *
1634
     * @return false|ResultInterface
1635
     */
1636
    public function get(?int $limit = null, int $offset = 0, bool $reset = true)
1637
    {
1638
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
708✔
1639
        if ($limitZeroAsAll && $limit === 0) {
708✔
1640
            $limit = null;
1✔
1641
        }
1642

1643
        if ($limit !== null) {
708✔
1644
            $this->limit($limit, $offset);
3✔
1645
        }
1646

1647
        $result = $this->testMode
708✔
1648
            ? $this->getCompiledSelect($reset)
2✔
1649
            : $this->db->query($this->compileSelect(), $this->binds, false);
706✔
1650

1651
        if ($reset) {
708✔
1652
            $this->resetSelect();
708✔
1653

1654
            // Clear our binds so we don't eat up memory
1655
            $this->binds = [];
708✔
1656
        }
1657

1658
        return $result;
708✔
1659
    }
1660

1661
    /**
1662
     * Generates a platform-specific query string that counts all records in
1663
     * the particular table
1664
     *
1665
     * @return int|string
1666
     */
1667
    public function countAll(bool $reset = true)
1668
    {
1669
        $table = $this->QBFrom[0];
6✔
1670

1671
        $sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' .
6✔
1672
            $this->db->protectIdentifiers($table, true, null, false);
6✔
1673

1674
        if ($this->testMode) {
6✔
1675
            return $sql;
1✔
1676
        }
1677

1678
        $query = $this->db->query($sql, null, false);
5✔
1679

1680
        if (empty($query->getResult())) {
5✔
1681
            return 0;
×
1682
        }
1683

1684
        $query = $query->getRow();
5✔
1685

1686
        if ($reset) {
5✔
1687
            $this->resetSelect();
5✔
1688
        }
1689

1690
        return (int) $query->numrows;
5✔
1691
    }
1692

1693
    /**
1694
     * Generates a platform-specific query string that counts all records
1695
     * returned by an Query Builder query.
1696
     *
1697
     * @return int|string
1698
     */
1699
    public function countAllResults(bool $reset = true)
1700
    {
1701
        // ORDER BY usage is often problematic here (most notably
1702
        // on Microsoft SQL Server) and ultimately unnecessary
1703
        // for selecting COUNT(*) ...
1704
        $orderBy = [];
162✔
1705

1706
        if (! empty($this->QBOrderBy)) {
162✔
1707
            $orderBy = $this->QBOrderBy;
×
1708

1709
            $this->QBOrderBy = null;
×
1710
        }
1711

1712
        // We cannot use a LIMIT when getting the single row COUNT(*) result
1713
        $limit = $this->QBLimit;
162✔
1714

1715
        $this->QBLimit = false;
162✔
1716

1717
        if ($this->QBDistinct === true || ! empty($this->QBGroupBy)) {
162✔
1718
            // We need to backup the original SELECT in case DBPrefix is used
1719
            $select = $this->QBSelect;
4✔
1720
            $sql    = $this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . $this->compileSelect() . "\n) CI_count_all_results";
4✔
1721

1722
            // Restore SELECT part
1723
            $this->QBSelect = $select;
4✔
1724
            unset($select);
4✔
1725
        } else {
1726
            $sql = $this->compileSelect($this->countString . $this->db->protectIdentifiers('numrows'));
158✔
1727
        }
1728

1729
        if ($this->testMode) {
162✔
1730
            return $sql;
9✔
1731
        }
1732

1733
        $result = $this->db->query($sql, $this->binds, false);
157✔
1734

1735
        if ($reset) {
157✔
1736
            $this->resetSelect();
148✔
1737
        } elseif (! isset($this->QBOrderBy)) {
15✔
1738
            $this->QBOrderBy = $orderBy;
×
1739
        }
1740

1741
        // Restore the LIMIT setting
1742
        $this->QBLimit = $limit;
157✔
1743

1744
        $row = $result instanceof ResultInterface ? $result->getRow() : null;
157✔
1745

1746
        if (empty($row)) {
157✔
1747
            return 0;
×
1748
        }
1749

1750
        return (int) $row->numrows;
157✔
1751
    }
1752

1753
    /**
1754
     * Compiles the set conditions and returns the sql statement
1755
     *
1756
     * @return array
1757
     */
1758
    public function getCompiledQBWhere()
1759
    {
1760
        return $this->QBWhere;
65✔
1761
    }
1762

1763
    /**
1764
     * Allows the where clause, limit and offset to be added directly
1765
     *
1766
     * @param array|string $where
1767
     *
1768
     * @return ResultInterface
1769
     */
1770
    public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bool $reset = true)
1771
    {
1772
        if ($where !== null) {
17✔
1773
            $this->where($where);
16✔
1774
        }
1775

1776
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
17✔
1777
        if ($limitZeroAsAll && $limit === 0) {
17✔
1778
            $limit = null;
×
1779
        }
1780

1781
        if ($limit !== null) {
17✔
1782
            $this->limit($limit, $offset);
3✔
1783
        }
1784

1785
        $result = $this->testMode
17✔
1786
            ? $this->getCompiledSelect($reset)
4✔
1787
            : $this->db->query($this->compileSelect(), $this->binds, false);
13✔
1788

1789
        if ($reset) {
17✔
1790
            $this->resetSelect();
17✔
1791

1792
            // Clear our binds so we don't eat up memory
1793
            $this->binds = [];
17✔
1794
        }
1795

1796
        return $result;
17✔
1797
    }
1798

1799
    /**
1800
     * Compiles batch insert/update/upsert strings and runs the queries
1801
     *
1802
     * @param '_deleteBatch'|'_insertBatch'|'_updateBatch'|'_upsertBatch' $renderMethod
1803
     *
1804
     * @return false|int|list<string> Number of rows inserted or FALSE on failure, SQL array when testMode
1805
     *
1806
     * @throws DatabaseException
1807
     */
1808
    protected function batchExecute(string $renderMethod, int $batchSize = 100)
1809
    {
1810
        if (empty($this->QBSet)) {
71✔
1811
            if ($this->db->DBDebug) {
5✔
1812
                throw new DatabaseException(trim($renderMethod, '_') . '() has no data.');
5✔
1813
            }
1814

1815
            return false; // @codeCoverageIgnore
×
1816
        }
1817

1818
        $table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);
66✔
1819

1820
        $affectedRows = 0;
66✔
1821
        $savedSQL     = [];
66✔
1822
        $cnt          = count($this->QBSet);
66✔
1823

1824
        // batch size 0 for unlimited
1825
        if ($batchSize === 0) {
66✔
1826
            $batchSize = $cnt;
×
1827
        }
1828

1829
        for ($i = 0, $total = $cnt; $i < $total; $i += $batchSize) {
66✔
1830
            $QBSet = array_slice($this->QBSet, $i, $batchSize);
66✔
1831

1832
            $sql = $this->{$renderMethod}($table, $this->QBKeys, $QBSet);
66✔
1833

1834
            if ($sql === '') {
63✔
1835
                return false; // @codeCoverageIgnore
×
1836
            }
1837

1838
            if ($this->testMode) {
63✔
1839
                $savedSQL[] = $sql;
3✔
1840
            } else {
1841
                $this->db->query($sql, null, false);
60✔
1842
                $affectedRows += $this->db->affectedRows();
58✔
1843
            }
1844
        }
1845

1846
        if (! $this->testMode) {
61✔
1847
            $this->resetWrite();
58✔
1848
        }
1849

1850
        return $this->testMode ? $savedSQL : $affectedRows;
61✔
1851
    }
1852

1853
    /**
1854
     * Allows a row or multiple rows to be set for batch inserts/upserts/updates
1855
     *
1856
     * @param array|object $set
1857
     * @param string       $alias alias for sql table
1858
     *
1859
     * @return $this|null
1860
     */
1861
    public function setData($set, ?bool $escape = null, string $alias = '')
1862
    {
1863
        if (empty($set)) {
66✔
1864
            if ($this->db->DBDebug) {
×
1865
                throw new DatabaseException('setData() has no data.');
×
1866
            }
1867

1868
            return null; // @codeCoverageIgnore
×
1869
        }
1870

1871
        $this->setAlias($alias);
66✔
1872

1873
        // this allows to set just one row at a time
1874
        if (is_object($set) || (! is_array(current($set)) && ! is_object(current($set)))) {
66✔
1875
            $set = [$set];
11✔
1876
        }
1877

1878
        $set = $this->batchObjectToArray($set);
66✔
1879

1880
        $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
66✔
1881

1882
        $keys = array_keys($this->objectToArray(current($set)));
66✔
1883
        sort($keys);
66✔
1884

1885
        foreach ($set as $row) {
66✔
1886
            $row = $this->objectToArray($row);
66✔
1887
            if (array_diff($keys, array_keys($row)) !== [] || array_diff(array_keys($row), $keys) !== []) {
66✔
1888
                // batchExecute() function returns an error on an empty array
1889
                $this->QBSet[] = [];
×
1890

1891
                return null;
×
1892
            }
1893

1894
            ksort($row); // puts $row in the same order as our keys
66✔
1895

1896
            $clean = [];
66✔
1897

1898
            foreach ($row as $rowValue) {
66✔
1899
                $clean[] = $escape ? $this->db->escape($rowValue) : $rowValue;
66✔
1900
            }
1901

1902
            $row = $clean;
66✔
1903

1904
            $this->QBSet[] = $row;
66✔
1905
        }
1906

1907
        foreach ($keys as $k) {
66✔
1908
            $k = $this->db->protectIdentifiers($k, false);
66✔
1909

1910
            if (! in_array($k, $this->QBKeys, true)) {
66✔
1911
                $this->QBKeys[] = $k;
66✔
1912
            }
1913
        }
1914

1915
        return $this;
66✔
1916
    }
1917

1918
    /**
1919
     * Compiles an upsert query and returns the sql
1920
     *
1921
     * @return string
1922
     *
1923
     * @throws DatabaseException
1924
     */
1925
    public function getCompiledUpsert()
1926
    {
1927
        [$currentTestMode, $this->testMode] = [$this->testMode, true];
3✔
1928

1929
        $sql = implode(";\n", $this->upsert());
3✔
1930

1931
        $this->testMode = $currentTestMode;
3✔
1932

1933
        return $this->compileFinalQuery($sql);
3✔
1934
    }
1935

1936
    /**
1937
     * Converts call to batchUpsert
1938
     *
1939
     * @param array|object|null $set
1940
     *
1941
     * @return false|int|list<string> Number of affected rows or FALSE on failure, SQL array when testMode
1942
     *
1943
     * @throws DatabaseException
1944
     */
1945
    public function upsert($set = null, ?bool $escape = null)
1946
    {
1947
        // if set() has been used merge QBSet with binds and then setData()
1948
        if ($set === null && ! is_array(current($this->QBSet))) {
10✔
1949
            $set = [];
2✔
1950

1951
            foreach ($this->QBSet as $field => $value) {
2✔
1952
                $k = trim($field, $this->db->escapeChar);
2✔
1953
                // use binds if available else use QBSet value but with RawSql to avoid escape
1954
                $set[$k] = isset($this->binds[$k]) ? $this->binds[$k][0] : new RawSql($value);
2✔
1955
            }
1956

1957
            $this->binds = [];
2✔
1958

1959
            $this->resetRun([
2✔
1960
                'QBSet'  => [],
2✔
1961
                'QBKeys' => [],
2✔
1962
            ]);
2✔
1963

1964
            $this->setData($set, true); // unescaped items are RawSql now
2✔
1965
        } elseif ($set !== null) {
8✔
1966
            $this->setData($set, $escape);
7✔
1967
        } // else setData() has already been used and we need to do nothing
1968

1969
        return $this->batchExecute('_upsertBatch');
10✔
1970
    }
1971

1972
    /**
1973
     * Compiles batch upsert strings and runs the queries
1974
     *
1975
     * @param array|object|null $set a dataset
1976
     *
1977
     * @return false|int|list<string> Number of affected rows or FALSE on failure, SQL array when testMode
1978
     *
1979
     * @throws DatabaseException
1980
     */
1981
    public function upsertBatch($set = null, ?bool $escape = null, int $batchSize = 100)
1982
    {
1983
        if (isset($this->QBOptions['setQueryAsData'])) {
11✔
1984
            $sql = $this->_upsertBatch($this->QBFrom[0], $this->QBKeys, []);
1✔
1985

1986
            if ($sql === '') {
1✔
1987
                return false; // @codeCoverageIgnore
×
1988
            }
1989

1990
            if ($this->testMode === false) {
1✔
1991
                $this->db->query($sql, null, false);
1✔
1992
            }
1993

1994
            $this->resetWrite();
1✔
1995

1996
            return $this->testMode ? $sql : $this->db->affectedRows();
1✔
1997
        }
1998

1999
        if ($set !== null) {
10✔
2000
            $this->setData($set, $escape);
8✔
2001
        }
2002

2003
        return $this->batchExecute('_upsertBatch', $batchSize);
10✔
2004
    }
2005

2006
    /**
2007
     * Generates a platform-specific upsertBatch string from the supplied data
2008
     *
2009
     * @used-by batchExecute()
2010
     *
2011
     * @param string                 $table  Protected table name
2012
     * @param list<string>           $keys   QBKeys
2013
     * @param list<list<int|string>> $values QBSet
2014
     */
2015
    protected function _upsertBatch(string $table, array $keys, array $values): string
2016
    {
2017
        $sql = $this->QBOptions['sql'] ?? '';
18✔
2018

2019
        // if this is the first iteration of batch then we need to build skeleton sql
2020
        if ($sql === '') {
18✔
2021
            $updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys)->QBOptions['updateFields'] ?? [];
18✔
2022

2023
            $sql = 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ")\n{:_table_:}ON DUPLICATE KEY UPDATE\n" . implode(
18✔
2024
                ",\n",
18✔
2025
                array_map(
18✔
2026
                    static fn ($key, $value): string => $table . '.' . $key . ($value instanceof RawSql ?
18✔
2027
                        ' = ' . $value :
2✔
2028
                        ' = VALUES(' . $value . ')'),
18✔
2029
                    array_keys($updateFields),
18✔
2030
                    $updateFields
18✔
2031
                )
18✔
2032
            );
18✔
2033

2034
            $this->QBOptions['sql'] = $sql;
18✔
2035
        }
2036

2037
        if (isset($this->QBOptions['setQueryAsData'])) {
18✔
2038
            $data = $this->QBOptions['setQueryAsData'] . "\n";
1✔
2039
        } else {
2040
            $data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
17✔
2041
        }
2042

2043
        return str_replace('{:_table_:}', $data, $sql);
18✔
2044
    }
2045

2046
    /**
2047
     * Set table alias for dataset pseudo table.
2048
     */
2049
    private function setAlias(string $alias): BaseBuilder
2050
    {
2051
        if ($alias !== '') {
66✔
2052
            $this->db->addTableAlias($alias);
7✔
2053
            $this->QBOptions['alias'] = $this->db->protectIdentifiers($alias);
7✔
2054
        }
2055

2056
        return $this;
66✔
2057
    }
2058

2059
    /**
2060
     * Sets update fields for upsert, update
2061
     *
2062
     * @param list<RawSql>|list<string>|string $set
2063
     * @param bool                             $addToDefault adds update fields to the default ones
2064
     * @param array|null                       $ignore       ignores items in set
2065
     *
2066
     * @return $this
2067
     */
2068
    public function updateFields($set, bool $addToDefault = false, ?array $ignore = null)
2069
    {
2070
        if (! empty($set)) {
36✔
2071
            if (! is_array($set)) {
36✔
2072
                $set = explode(',', $set);
5✔
2073
            }
2074

2075
            foreach ($set as $key => $value) {
36✔
2076
                if (! ($value instanceof RawSql)) {
36✔
2077
                    $value = $this->db->protectIdentifiers($value);
36✔
2078
                }
2079

2080
                if (is_numeric($key)) {
36✔
2081
                    $key = $value;
36✔
2082
                }
2083

2084
                if ($ignore === null || ! in_array($key, $ignore, true)) {
36✔
2085
                    if ($addToDefault) {
36✔
2086
                        $this->QBOptions['updateFieldsAdditional'][$this->db->protectIdentifiers($key)] = $value;
3✔
2087
                    } else {
2088
                        $this->QBOptions['updateFields'][$this->db->protectIdentifiers($key)] = $value;
36✔
2089
                    }
2090
                }
2091
            }
2092

2093
            if ($addToDefault === false && isset($this->QBOptions['updateFieldsAdditional'], $this->QBOptions['updateFields'])) {
36✔
2094
                $this->QBOptions['updateFields'] = array_merge($this->QBOptions['updateFields'], $this->QBOptions['updateFieldsAdditional']);
3✔
2095

2096
                unset($this->QBOptions['updateFieldsAdditional']);
3✔
2097
            }
2098
        }
2099

2100
        return $this;
36✔
2101
    }
2102

2103
    /**
2104
     * Sets constraints for batch upsert, update
2105
     *
2106
     * @param array|RawSql|string $set a string of columns, key value pairs, or RawSql
2107
     *
2108
     * @return $this
2109
     */
2110
    public function onConstraint($set)
2111
    {
2112
        if (! empty($set)) {
45✔
2113
            if (is_string($set)) {
42✔
2114
                $set = explode(',', $set);
26✔
2115

2116
                $set = array_map(static fn ($key): string => trim($key), $set);
26✔
2117
            }
2118

2119
            if ($set instanceof RawSql) {
42✔
2120
                $set = [$set];
2✔
2121
            }
2122

2123
            foreach ($set as $key => $value) {
42✔
2124
                if (! ($value instanceof RawSql)) {
42✔
2125
                    $value = $this->db->protectIdentifiers($value);
39✔
2126
                }
2127

2128
                if (is_string($key)) {
42✔
2129
                    $key = $this->db->protectIdentifiers($key);
3✔
2130
                }
2131

2132
                $this->QBOptions['constraints'][$key] = $value;
42✔
2133
            }
2134
        }
2135

2136
        return $this;
45✔
2137
    }
2138

2139
    /**
2140
     * Sets data source as a query for insertBatch()/updateBatch()/upsertBatch()/deleteBatch()
2141
     *
2142
     * @param BaseBuilder|RawSql $query
2143
     * @param array|string|null  $columns an array or comma delimited string of columns
2144
     */
2145
    public function setQueryAsData($query, ?string $alias = null, $columns = null): BaseBuilder
2146
    {
2147
        if (is_string($query)) {
5✔
2148
            throw new InvalidArgumentException('$query parameter must be BaseBuilder or RawSql class.');
×
2149
        }
2150

2151
        if ($query instanceof BaseBuilder) {
5✔
2152
            $query = $query->getCompiledSelect();
4✔
2153
        } elseif ($query instanceof RawSql) {
1✔
2154
            $query = $query->__toString();
1✔
2155
        }
2156

2157
        if (is_string($query)) {
5✔
2158
            if ($columns !== null && is_string($columns)) {
5✔
2159
                $columns = explode(',', $columns);
1✔
2160
                $columns = array_map(static fn ($key): string => trim($key), $columns);
1✔
2161
            }
2162

2163
            $columns = (array) $columns;
5✔
2164

2165
            if ($columns === []) {
5✔
2166
                $columns = $this->fieldsFromQuery($query);
4✔
2167
            }
2168

2169
            if ($alias !== null) {
5✔
2170
                $this->setAlias($alias);
1✔
2171
            }
2172

2173
            foreach ($columns as $key => $value) {
5✔
2174
                $columns[$key] = $this->db->escapeChar . $value . $this->db->escapeChar;
5✔
2175
            }
2176

2177
            $this->QBOptions['setQueryAsData'] = $query;
5✔
2178
            $this->QBKeys                      = $columns;
5✔
2179
            $this->QBSet                       = [];
5✔
2180
        }
2181

2182
        return $this;
5✔
2183
    }
2184

2185
    /**
2186
     * Gets column names from a select query
2187
     */
2188
    protected function fieldsFromQuery(string $sql): array
2189
    {
2190
        return $this->db->query('SELECT * FROM (' . $sql . ') _u_ LIMIT 1')->getFieldNames();
4✔
2191
    }
2192

2193
    /**
2194
     * Converts value array of array to array of strings
2195
     */
2196
    protected function formatValues(array $values): array
2197
    {
2198
        return array_map(static fn ($index): string => '(' . implode(',', $index) . ')', $values);
44✔
2199
    }
2200

2201
    /**
2202
     * Compiles batch insert strings and runs the queries
2203
     *
2204
     * @param array|object|null $set a dataset
2205
     *
2206
     * @return false|int|list<string> Number of rows inserted or FALSE on no data to perform an insert operation, SQL array when testMode
2207
     */
2208
    public function insertBatch($set = null, ?bool $escape = null, int $batchSize = 100)
2209
    {
2210
        if (isset($this->QBOptions['setQueryAsData'])) {
29✔
2211
            $sql = $this->_insertBatch($this->QBFrom[0], $this->QBKeys, []);
2✔
2212

2213
            if ($sql === '') {
2✔
2214
                return false; // @codeCoverageIgnore
×
2215
            }
2216

2217
            if ($this->testMode === false) {
2✔
2218
                $this->db->query($sql, null, false);
2✔
2219
            }
2220

2221
            $this->resetWrite();
2✔
2222

2223
            return $this->testMode ? $sql : $this->db->affectedRows();
2✔
2224
        }
2225

2226
        if ($set !== null && $set !== []) {
29✔
2227
            $this->setData($set, $escape);
26✔
2228
        }
2229

2230
        return $this->batchExecute('_insertBatch', $batchSize);
29✔
2231
    }
2232

2233
    /**
2234
     * Generates a platform-specific insert string from the supplied data.
2235
     *
2236
     * @used-by batchExecute()
2237
     *
2238
     * @param string                 $table  Protected table name
2239
     * @param list<string>           $keys   QBKeys
2240
     * @param list<list<int|string>> $values QBSet
2241
     */
2242
    protected function _insertBatch(string $table, array $keys, array $values): string
2243
    {
2244
        $sql = $this->QBOptions['sql'] ?? '';
27✔
2245

2246
        // if this is the first iteration of batch then we need to build skeleton sql
2247
        if ($sql === '') {
27✔
2248
            $sql = 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table
27✔
2249
                . ' (' . implode(', ', $keys) . ")\n{:_table_:}";
27✔
2250

2251
            $this->QBOptions['sql'] = $sql;
27✔
2252
        }
2253

2254
        if (isset($this->QBOptions['setQueryAsData'])) {
27✔
2255
            $data = $this->QBOptions['setQueryAsData'];
2✔
2256
        } else {
2257
            $data = 'VALUES ' . implode(', ', $this->formatValues($values));
27✔
2258
        }
2259

2260
        return str_replace('{:_table_:}', $data, $sql);
27✔
2261
    }
2262

2263
    /**
2264
     * Allows key/value pairs to be set for batch inserts
2265
     *
2266
     * @param mixed $key
2267
     *
2268
     * @return $this|null
2269
     *
2270
     * @deprecated
2271
     */
2272
    public function setInsertBatch($key, string $value = '', ?bool $escape = null)
2273
    {
2274
        if (! is_array($key)) {
1✔
2275
            $key = [[$key => $value]];
×
2276
        }
2277

2278
        return $this->setData($key, $escape);
1✔
2279
    }
2280

2281
    /**
2282
     * Compiles an insert query and returns the sql
2283
     *
2284
     * @return bool|string
2285
     *
2286
     * @throws DatabaseException
2287
     */
2288
    public function getCompiledInsert(bool $reset = true)
2289
    {
2290
        if ($this->validateInsert() === false) {
6✔
2291
            return false;
×
2292
        }
2293

2294
        $sql = $this->_insert(
6✔
2295
            $this->db->protectIdentifiers(
6✔
2296
                $this->removeAlias($this->QBFrom[0]),
6✔
2297
                true,
6✔
2298
                null,
6✔
2299
                false
6✔
2300
            ),
6✔
2301
            array_keys($this->QBSet),
6✔
2302
            array_values($this->QBSet)
6✔
2303
        );
6✔
2304

2305
        if ($reset) {
6✔
2306
            $this->resetWrite();
6✔
2307
        }
2308

2309
        return $this->compileFinalQuery($sql);
6✔
2310
    }
2311

2312
    /**
2313
     * Compiles an insert string and runs the query
2314
     *
2315
     * @param array|object|null $set
2316
     *
2317
     * @return BaseResult|bool|Query
2318
     *
2319
     * @throws DatabaseException
2320
     */
2321
    public function insert($set = null, ?bool $escape = null)
2322
    {
2323
        if ($set !== null) {
681✔
2324
            $this->set($set, '', $escape);
647✔
2325
        }
2326

2327
        if ($this->validateInsert() === false) {
681✔
2328
            return false;
×
2329
        }
2330

2331
        $sql = $this->_insert(
680✔
2332
            $this->db->protectIdentifiers(
680✔
2333
                $this->removeAlias($this->QBFrom[0]),
680✔
2334
                true,
680✔
2335
                $escape,
680✔
2336
                false
680✔
2337
            ),
680✔
2338
            array_keys($this->QBSet),
680✔
2339
            array_values($this->QBSet)
680✔
2340
        );
680✔
2341

2342
        if (! $this->testMode) {
680✔
2343
            $this->resetWrite();
676✔
2344

2345
            $result = $this->db->query($sql, $this->binds, false);
676✔
2346

2347
            // Clear our binds so we don't eat up memory
2348
            $this->binds = [];
676✔
2349

2350
            return $result;
676✔
2351
        }
2352

2353
        return false;
5✔
2354
    }
2355

2356
    /**
2357
     * @internal This is a temporary solution.
2358
     *
2359
     * @see https://github.com/codeigniter4/CodeIgniter4/pull/5376
2360
     *
2361
     * @TODO Fix a root cause, and this method should be removed.
2362
     */
2363
    protected function removeAlias(string $from): string
2364
    {
2365
        if (str_contains($from, ' ')) {
685✔
2366
            // if the alias is written with the AS keyword, remove it
2367
            $from = preg_replace('/\s+AS\s+/i', ' ', $from);
2✔
2368

2369
            $parts = explode(' ', $from);
2✔
2370
            $from  = $parts[0];
2✔
2371
        }
2372

2373
        return $from;
685✔
2374
    }
2375

2376
    /**
2377
     * This method is used by both insert() and getCompiledInsert() to
2378
     * validate that the there data is actually being set and that table
2379
     * has been chosen to be inserted into.
2380
     *
2381
     * @throws DatabaseException
2382
     */
2383
    protected function validateInsert(): bool
2384
    {
2385
        if (empty($this->QBSet)) {
681✔
2386
            if ($this->db->DBDebug) {
1✔
2387
                throw new DatabaseException('You must use the "set" method to insert an entry.');
1✔
2388
            }
2389

2390
            return false; // @codeCoverageIgnore
×
2391
        }
2392

2393
        return true;
680✔
2394
    }
2395

2396
    /**
2397
     * Generates a platform-specific insert string from the supplied data
2398
     *
2399
     * @param string           $table         Protected table name
2400
     * @param list<string>     $keys          Keys of QBSet
2401
     * @param list<int|string> $unescapedKeys Values of QBSet
2402
     */
2403
    protected function _insert(string $table, array $keys, array $unescapedKeys): string
2404
    {
2405
        return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')';
674✔
2406
    }
2407

2408
    /**
2409
     * Compiles a replace into string and runs the query
2410
     *
2411
     * @return BaseResult|false|Query|string
2412
     *
2413
     * @throws DatabaseException
2414
     */
2415
    public function replace(?array $set = null)
2416
    {
2417
        if ($set !== null) {
8✔
2418
            $this->set($set);
7✔
2419
        }
2420

2421
        if (empty($this->QBSet)) {
8✔
2422
            if ($this->db->DBDebug) {
1✔
2423
                throw new DatabaseException('You must use the "set" method to update an entry.');
1✔
2424
            }
2425

2426
            return false; // @codeCoverageIgnore
×
2427
        }
2428

2429
        $table = $this->QBFrom[0];
7✔
2430

2431
        $sql = $this->_replace($table, array_keys($this->QBSet), array_values($this->QBSet));
7✔
2432

2433
        $this->resetWrite();
7✔
2434

2435
        return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false);
7✔
2436
    }
2437

2438
    /**
2439
     * Generates a platform-specific replace string from the supplied data
2440
     *
2441
     * @param string           $table  Protected table name
2442
     * @param list<string>     $keys   Keys of QBSet
2443
     * @param list<int|string> $values Values of QBSet
2444
     */
2445
    protected function _replace(string $table, array $keys, array $values): string
2446
    {
2447
        return 'REPLACE INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
7✔
2448
    }
2449

2450
    /**
2451
     * Groups tables in FROM clauses if needed, so there is no confusion
2452
     * about operator precedence.
2453
     *
2454
     * Note: This is only used (and overridden) by MySQL and SQLSRV.
2455
     */
2456
    protected function _fromTables(): string
2457
    {
2458
        return implode(', ', $this->QBFrom);
843✔
2459
    }
2460

2461
    /**
2462
     * Compiles an update query and returns the sql
2463
     *
2464
     * @return bool|string
2465
     */
2466
    public function getCompiledUpdate(bool $reset = true)
2467
    {
2468
        if ($this->validateUpdate() === false) {
13✔
2469
            return false;
×
2470
        }
2471

2472
        $sql = $this->_update($this->QBFrom[0], $this->QBSet);
13✔
2473

2474
        if ($reset) {
13✔
2475
            $this->resetWrite();
13✔
2476
        }
2477

2478
        return $this->compileFinalQuery($sql);
13✔
2479
    }
2480

2481
    /**
2482
     * Compiles an update string and runs the query.
2483
     *
2484
     * @param array|object|null        $set
2485
     * @param array|RawSql|string|null $where
2486
     *
2487
     * @throws DatabaseException
2488
     */
2489
    public function update($set = null, $where = null, ?int $limit = null): bool
2490
    {
2491
        if ($set !== null) {
101✔
2492
            $this->set($set);
47✔
2493
        }
2494

2495
        if ($this->validateUpdate() === false) {
101✔
2496
            return false;
×
2497
        }
2498

2499
        if ($where !== null) {
100✔
2500
            $this->where($where);
8✔
2501
        }
2502

2503
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
100✔
2504
        if ($limitZeroAsAll && $limit === 0) {
100✔
2505
            $limit = null;
×
2506
        }
2507

2508
        if ($limit !== null) {
100✔
2509
            if (! $this->canLimitWhereUpdates) {
3✔
2510
                throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.');
2✔
2511
            }
2512

2513
            $this->limit($limit);
3✔
2514
        }
2515

2516
        $sql = $this->_update($this->QBFrom[0], $this->QBSet);
100✔
2517

2518
        if (! $this->testMode) {
100✔
2519
            $this->resetWrite();
87✔
2520

2521
            $result = $this->db->query($sql, $this->binds, false);
87✔
2522

2523
            if ($result !== false) {
87✔
2524
                // Clear our binds so we don't eat up memory
2525
                $this->binds = [];
84✔
2526

2527
                return true;
84✔
2528
            }
2529

2530
            return false;
3✔
2531
        }
2532

2533
        return true;
13✔
2534
    }
2535

2536
    /**
2537
     * Generates a platform-specific update string from the supplied data
2538
     *
2539
     * @param string                $table  Protected table name
2540
     * @param array<string, string> $values QBSet
2541
     */
2542
    protected function _update(string $table, array $values): string
2543
    {
2544
        $valStr = [];
107✔
2545

2546
        foreach ($values as $key => $val) {
107✔
2547
            $valStr[] = $key . ' = ' . $val;
107✔
2548
        }
2549

2550
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
107✔
2551
        if ($limitZeroAsAll) {
107✔
2552
            return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
107✔
2553
                . $this->compileWhereHaving('QBWhere')
107✔
2554
                . $this->compileOrderBy()
107✔
2555
                . ($this->QBLimit ? $this->_limit(' ', true) : '');
107✔
2556
        }
2557

2558
        return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
×
2559
            . $this->compileWhereHaving('QBWhere')
×
2560
            . $this->compileOrderBy()
×
2561
            . ($this->QBLimit !== false ? $this->_limit(' ', true) : '');
×
2562
    }
2563

2564
    /**
2565
     * This method is used by both update() and getCompiledUpdate() to
2566
     * validate that data is actually being set and that a table has been
2567
     * chosen to be updated.
2568
     *
2569
     * @throws DatabaseException
2570
     */
2571
    protected function validateUpdate(): bool
2572
    {
2573
        if (empty($this->QBSet)) {
102✔
2574
            if ($this->db->DBDebug) {
1✔
2575
                throw new DatabaseException('You must use the "set" method to update an entry.');
1✔
2576
            }
2577

2578
            return false; // @codeCoverageIgnore
×
2579
        }
2580

2581
        return true;
101✔
2582
    }
2583

2584
    /**
2585
     * Sets data and calls batchExecute to run queries
2586
     *
2587
     * @param array|object|null        $set         a dataset
2588
     * @param array|RawSql|string|null $constraints
2589
     *
2590
     * @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
2591
     */
2592
    public function updateBatch($set = null, $constraints = null, int $batchSize = 100)
2593
    {
2594
        $this->onConstraint($constraints);
22✔
2595

2596
        if (isset($this->QBOptions['setQueryAsData'])) {
22✔
2597
            $sql = $this->_updateBatch($this->QBFrom[0], $this->QBKeys, []);
1✔
2598

2599
            if ($sql === '') {
1✔
2600
                return false; // @codeCoverageIgnore
×
2601
            }
2602

2603
            if ($this->testMode === false) {
1✔
2604
                $this->db->query($sql, null, false);
1✔
2605
            }
2606

2607
            $this->resetWrite();
1✔
2608

2609
            return $this->testMode ? $sql : $this->db->affectedRows();
1✔
2610
        }
2611

2612
        if ($set !== null && $set !== []) {
21✔
2613
            $this->setData($set, true);
14✔
2614
        }
2615

2616
        return $this->batchExecute('_updateBatch', $batchSize);
21✔
2617
    }
2618

2619
    /**
2620
     * Generates a platform-specific batch update string from the supplied data
2621
     *
2622
     * @used-by batchExecute()
2623
     *
2624
     * @param string                 $table  Protected table name
2625
     * @param list<string>           $keys   QBKeys
2626
     * @param list<list<int|string>> $values QBSet
2627
     */
2628
    protected function _updateBatch(string $table, array $keys, array $values): string
2629
    {
2630
        $sql = $this->QBOptions['sql'] ?? '';
17✔
2631

2632
        // if this is the first iteration of batch then we need to build skeleton sql
2633
        if ($sql === '') {
17✔
2634
            $constraints = $this->QBOptions['constraints'] ?? [];
17✔
2635

2636
            if ($constraints === []) {
17✔
2637
                if ($this->db->DBDebug) {
2✔
2638
                    throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
2✔
2639
                }
2640

2641
                return ''; // @codeCoverageIgnore
×
2642
            }
2643

2644
            $updateFields = $this->QBOptions['updateFields'] ??
15✔
2645
                $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
15✔
2646
                [];
12✔
2647

2648
            $alias = $this->QBOptions['alias'] ?? '_u';
15✔
2649

2650
            $sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
15✔
2651

2652
            $sql .= "SET\n";
15✔
2653

2654
            $sql .= implode(
15✔
2655
                ",\n",
15✔
2656
                array_map(
15✔
2657
                    static fn ($key, $value): string => $key . ($value instanceof RawSql ?
15✔
2658
                        ' = ' . $value :
2✔
2659
                        ' = ' . $alias . '.' . $value),
15✔
2660
                    array_keys($updateFields),
15✔
2661
                    $updateFields
15✔
2662
                )
15✔
2663
            ) . "\n";
15✔
2664

2665
            $sql .= "FROM (\n{:_table_:}";
15✔
2666

2667
            $sql .= ') ' . $alias . "\n";
15✔
2668

2669
            $sql .= 'WHERE ' . implode(
15✔
2670
                ' AND ',
15✔
2671
                array_map(
15✔
2672
                    static fn ($key, $value) => (
15✔
2673
                        ($value instanceof RawSql && is_string($key))
15✔
2674
                        ?
15✔
2675
                        $table . '.' . $key . ' = ' . $value
1✔
2676
                        :
15✔
2677
                        (
15✔
2678
                            $value instanceof RawSql
14✔
2679
                            ?
14✔
2680
                            $value
3✔
2681
                            :
14✔
2682
                            $table . '.' . $value . ' = ' . $alias . '.' . $value
15✔
2683
                        )
15✔
2684
                    ),
15✔
2685
                    array_keys($constraints),
15✔
2686
                    $constraints
15✔
2687
                )
15✔
2688
            );
15✔
2689

2690
            $this->QBOptions['sql'] = $sql;
15✔
2691
        }
2692

2693
        if (isset($this->QBOptions['setQueryAsData'])) {
15✔
2694
            $data = $this->QBOptions['setQueryAsData'];
1✔
2695
        } else {
2696
            $data = implode(
14✔
2697
                " UNION ALL\n",
14✔
2698
                array_map(
14✔
2699
                    static fn ($value): string => 'SELECT ' . implode(', ', array_map(
14✔
2700
                        static fn ($key, $index): string => $index . ' ' . $key,
14✔
2701
                        $keys,
14✔
2702
                        $value
14✔
2703
                    )),
14✔
2704
                    $values
14✔
2705
                )
14✔
2706
            ) . "\n";
14✔
2707
        }
2708

2709
        return str_replace('{:_table_:}', $data, $sql);
15✔
2710
    }
2711

2712
    /**
2713
     * Allows key/value pairs to be set for batch updating
2714
     *
2715
     * @param array|object $key
2716
     *
2717
     * @return $this
2718
     *
2719
     * @throws DatabaseException
2720
     *
2721
     * @deprecated
2722
     */
2723
    public function setUpdateBatch($key, string $index = '', ?bool $escape = null)
2724
    {
2725
        if ($index !== '') {
2✔
2726
            $this->onConstraint($index);
2✔
2727
        }
2728

2729
        $this->setData($key, $escape);
2✔
2730

2731
        return $this;
2✔
2732
    }
2733

2734
    /**
2735
     * Compiles a delete string and runs "DELETE FROM table"
2736
     *
2737
     * @return bool|string TRUE on success, FALSE on failure, string on testMode
2738
     */
2739
    public function emptyTable()
2740
    {
2741
        $table = $this->QBFrom[0];
4✔
2742

2743
        $sql = $this->_delete($table);
4✔
2744

2745
        if ($this->testMode) {
4✔
2746
            return $sql;
1✔
2747
        }
2748

2749
        $this->resetWrite();
3✔
2750

2751
        return $this->db->query($sql, null, false);
3✔
2752
    }
2753

2754
    /**
2755
     * Compiles a truncate string and runs the query
2756
     * If the database does not support the truncate() command
2757
     * This function maps to "DELETE FROM table"
2758
     *
2759
     * @return bool|string TRUE on success, FALSE on failure, string on testMode
2760
     */
2761
    public function truncate()
2762
    {
2763
        $table = $this->QBFrom[0];
608✔
2764

2765
        $sql = $this->_truncate($table);
608✔
2766

2767
        if ($this->testMode) {
608✔
2768
            return $sql;
2✔
2769
        }
2770

2771
        $this->resetWrite();
607✔
2772

2773
        return $this->db->query($sql, null, false);
607✔
2774
    }
2775

2776
    /**
2777
     * Generates a platform-specific truncate string from the supplied data
2778
     *
2779
     * If the database does not support the truncate() command,
2780
     * then this method maps to 'DELETE FROM table'
2781
     *
2782
     * @param string $table Protected table name
2783
     */
2784
    protected function _truncate(string $table): string
2785
    {
2786
        return 'TRUNCATE ' . $table;
596✔
2787
    }
2788

2789
    /**
2790
     * Compiles a delete query string and returns the sql
2791
     */
2792
    public function getCompiledDelete(bool $reset = true): string
2793
    {
2794
        $sql = $this->testMode()->delete('', null, $reset);
3✔
2795
        $this->testMode(false);
3✔
2796

2797
        return $this->compileFinalQuery($sql);
3✔
2798
    }
2799

2800
    /**
2801
     * Compiles a delete string and runs the query
2802
     *
2803
     * @param array|RawSql|string $where
2804
     *
2805
     * @return bool|string Returns a SQL string if in test mode.
2806
     *
2807
     * @throws DatabaseException
2808
     */
2809
    public function delete($where = '', ?int $limit = null, bool $resetData = true)
2810
    {
2811
        $table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);
640✔
2812

2813
        if ($where !== '') {
640✔
2814
            $this->where($where);
4✔
2815
        }
2816

2817
        if (empty($this->QBWhere)) {
640✔
2818
            if ($this->db->DBDebug) {
4✔
2819
                throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.');
4✔
2820
            }
2821

2822
            return false; // @codeCoverageIgnore
×
2823
        }
2824

2825
        $sql = $this->_delete($this->removeAlias($table));
640✔
2826

2827
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
640✔
2828
        if ($limitZeroAsAll && $limit === 0) {
640✔
2829
            $limit = null;
×
2830
        }
2831

2832
        if ($limit !== null) {
640✔
2833
            $this->QBLimit = $limit;
1✔
2834
        }
2835

2836
        if (! empty($this->QBLimit)) {
640✔
2837
            if (! $this->canLimitDeletes) {
2✔
2838
                throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.');
1✔
2839
            }
2840

2841
            $sql = $this->_limit($sql, true);
2✔
2842
        }
2843

2844
        if ($resetData) {
640✔
2845
            $this->resetWrite();
640✔
2846
        }
2847

2848
        return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false);
640✔
2849
    }
2850

2851
    /**
2852
     * Sets data and calls batchExecute to run queries
2853
     *
2854
     * @param array|object|null $set         a dataset
2855
     * @param array|RawSql|null $constraints
2856
     *
2857
     * @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
2858
     */
2859
    public function deleteBatch($set = null, $constraints = null, int $batchSize = 100)
2860
    {
2861
        $this->onConstraint($constraints);
3✔
2862

2863
        if (isset($this->QBOptions['setQueryAsData'])) {
3✔
2864
            $sql = $this->_deleteBatch($this->QBFrom[0], $this->QBKeys, []);
1✔
2865

2866
            if ($sql === '') {
1✔
2867
                return false; // @codeCoverageIgnore
×
2868
            }
2869

2870
            if ($this->testMode === false) {
1✔
2871
                $this->db->query($sql, null, false);
1✔
2872
            }
2873

2874
            $this->resetWrite();
1✔
2875

2876
            return $this->testMode ? $sql : $this->db->affectedRows();
1✔
2877
        }
2878

2879
        if ($set !== null && $set !== []) {
2✔
2880
            $this->setData($set, true);
×
2881
        }
2882

2883
        return $this->batchExecute('_deleteBatch', $batchSize);
2✔
2884
    }
2885

2886
    /**
2887
     * Generates a platform-specific batch update string from the supplied data
2888
     *
2889
     * @used-by batchExecute()
2890
     *
2891
     * @param string           $table  Protected table name
2892
     * @param list<string>     $keys   QBKeys
2893
     * @param list<int|string> $values QBSet
2894
     */
2895
    protected function _deleteBatch(string $table, array $keys, array $values): string
2896
    {
2897
        $sql = $this->QBOptions['sql'] ?? '';
3✔
2898

2899
        // if this is the first iteration of batch then we need to build skeleton sql
2900
        if ($sql === '') {
3✔
2901
            $constraints = $this->QBOptions['constraints'] ?? [];
3✔
2902

2903
            if ($constraints === []) {
3✔
2904
                if ($this->db->DBDebug) {
×
2905
                    throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
×
2906
                }
2907

2908
                return ''; // @codeCoverageIgnore
×
2909
            }
2910

2911
            $alias = $this->QBOptions['alias'] ?? '_u';
3✔
2912

2913
            $sql = 'DELETE ' . $table . ' FROM ' . $table . "\n";
3✔
2914

2915
            $sql .= "INNER JOIN (\n{:_table_:}";
3✔
2916

2917
            $sql .= ') ' . $alias . "\n";
3✔
2918

2919
            $sql .= 'ON ' . implode(
3✔
2920
                ' AND ',
3✔
2921
                array_map(
3✔
2922
                    static fn ($key, $value) => (
3✔
2923
                        $value instanceof RawSql ?
3✔
2924
                        $value :
×
2925
                        (
3✔
2926
                            is_string($key) ?
3✔
2927
                            $table . '.' . $key . ' = ' . $alias . '.' . $value :
2✔
2928
                            $table . '.' . $value . ' = ' . $alias . '.' . $value
3✔
2929
                        )
3✔
2930
                    ),
3✔
2931
                    array_keys($constraints),
3✔
2932
                    $constraints
3✔
2933
                )
3✔
2934
            );
3✔
2935

2936
            // convert binds in where
2937
            foreach ($this->QBWhere as $key => $where) {
3✔
2938
                foreach ($this->binds as $field => $bind) {
2✔
2939
                    $this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']);
×
2940
                }
2941
            }
2942

2943
            $sql .= ' ' . $this->compileWhereHaving('QBWhere');
3✔
2944

2945
            $this->QBOptions['sql'] = trim($sql);
3✔
2946
        }
2947

2948
        if (isset($this->QBOptions['setQueryAsData'])) {
3✔
2949
            $data = $this->QBOptions['setQueryAsData'];
1✔
2950
        } else {
2951
            $data = implode(
2✔
2952
                " UNION ALL\n",
2✔
2953
                array_map(
2✔
2954
                    static fn ($value): string => 'SELECT ' . implode(', ', array_map(
2✔
2955
                        static fn ($key, $index): string => $index . ' ' . $key,
2✔
2956
                        $keys,
2✔
2957
                        $value
2✔
2958
                    )),
2✔
2959
                    $values
2✔
2960
                )
2✔
2961
            ) . "\n";
2✔
2962
        }
2963

2964
        return str_replace('{:_table_:}', $data, $sql);
3✔
2965
    }
2966

2967
    /**
2968
     * Increments a numeric column by the specified value.
2969
     *
2970
     * @return bool
2971
     */
2972
    public function increment(string $column, int $value = 1)
2973
    {
2974
        $column = $this->db->protectIdentifiers($column);
3✔
2975

2976
        $sql = $this->_update($this->QBFrom[0], [$column => "{$column} + {$value}"]);
3✔
2977

2978
        if (! $this->testMode) {
3✔
2979
            $this->resetWrite();
3✔
2980

2981
            return $this->db->query($sql, $this->binds, false);
3✔
2982
        }
2983

2984
        return true;
×
2985
    }
2986

2987
    /**
2988
     * Decrements a numeric column by the specified value.
2989
     *
2990
     * @return bool
2991
     */
2992
    public function decrement(string $column, int $value = 1)
2993
    {
2994
        $column = $this->db->protectIdentifiers($column);
3✔
2995

2996
        $sql = $this->_update($this->QBFrom[0], [$column => "{$column}-{$value}"]);
3✔
2997

2998
        if (! $this->testMode) {
3✔
2999
            $this->resetWrite();
3✔
3000

3001
            return $this->db->query($sql, $this->binds, false);
3✔
3002
        }
3003

3004
        return true;
×
3005
    }
3006

3007
    /**
3008
     * Generates a platform-specific delete string from the supplied data
3009
     *
3010
     * @param string $table Protected table name
3011
     */
3012
    protected function _delete(string $table): string
3013
    {
3014
        return 'DELETE ' . $this->compileIgnore('delete') . 'FROM ' . $table . $this->compileWhereHaving('QBWhere');
643✔
3015
    }
3016

3017
    /**
3018
     * Used to track SQL statements written with aliased tables.
3019
     *
3020
     * @param array|string $table The table to inspect
3021
     *
3022
     * @return string|null
3023
     */
3024
    protected function trackAliases($table)
3025
    {
3026
        if (is_array($table)) {
933✔
3027
            foreach ($table as $t) {
×
3028
                $this->trackAliases($t);
×
3029
            }
3030

3031
            return null;
×
3032
        }
3033

3034
        // Does the string contain a comma?  If so, we need to separate
3035
        // the string into discreet statements
3036
        if (str_contains($table, ',')) {
933✔
3037
            return $this->trackAliases(explode(',', $table));
×
3038
        }
3039

3040
        // if a table alias is used we can recognize it by a space
3041
        if (str_contains($table, ' ')) {
933✔
3042
            // if the alias is written with the AS keyword, remove it
3043
            $table = preg_replace('/\s+AS\s+/i', ' ', $table);
16✔
3044

3045
            // Grab the alias
3046
            $alias = trim(strrchr($table, ' '));
16✔
3047

3048
            // Store the alias, if it doesn't already exist
3049
            $this->db->addTableAlias($alias);
16✔
3050
        }
3051

3052
        return null;
933✔
3053
    }
3054

3055
    /**
3056
     * Compile the SELECT statement
3057
     *
3058
     * Generates a query string based on which functions were used.
3059
     * Should not be called directly.
3060
     *
3061
     * @param mixed $selectOverride
3062
     */
3063
    protected function compileSelect($selectOverride = false): string
3064
    {
3065
        if ($selectOverride !== false) {
869✔
3066
            $sql = $selectOverride;
158✔
3067
        } else {
3068
            $sql = (! $this->QBDistinct) ? 'SELECT ' : 'SELECT DISTINCT ';
866✔
3069

3070
            if (empty($this->QBSelect)) {
866✔
3071
                $sql .= '*';
786✔
3072
            } else {
3073
                // Cycle through the "select" portion of the query and prep each column name.
3074
                // The reason we protect identifiers here rather than in the select() function
3075
                // is because until the user calls the from() function we don't know if there are aliases
3076
                foreach ($this->QBSelect as $key => $val) {
742✔
3077
                    if ($val instanceof RawSql) {
742✔
3078
                        $this->QBSelect[$key] = (string) $val;
5✔
3079
                    } else {
3080
                        $protect              = $this->QBNoEscape[$key] ?? null;
740✔
3081
                        $this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $protect);
740✔
3082
                    }
3083
                }
3084

3085
                $sql .= implode(', ', $this->QBSelect);
742✔
3086
            }
3087
        }
3088

3089
        if (! empty($this->QBFrom)) {
869✔
3090
            $sql .= "\nFROM " . $this->_fromTables();
869✔
3091
        }
3092

3093
        if (! empty($this->QBJoin)) {
869✔
3094
            $sql .= "\n" . implode("\n", $this->QBJoin);
15✔
3095
        }
3096

3097
        $sql .= $this->compileWhereHaving('QBWhere')
869✔
3098
            . $this->compileGroupBy()
869✔
3099
            . $this->compileWhereHaving('QBHaving')
869✔
3100
            . $this->compileOrderBy();
869✔
3101

3102
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
869✔
3103
        if ($limitZeroAsAll) {
869✔
3104
            if ($this->QBLimit) {
868✔
3105
                $sql = $this->_limit($sql . "\n");
87✔
3106
            }
3107
        } elseif ($this->QBLimit !== false || $this->QBOffset) {
2✔
3108
            $sql = $this->_limit($sql . "\n");
2✔
3109
        }
3110

3111
        return $this->unionInjection($sql);
869✔
3112
    }
3113

3114
    /**
3115
     * Checks if the ignore option is supported by
3116
     * the Database Driver for the specific statement.
3117
     *
3118
     * @return string
3119
     */
3120
    protected function compileIgnore(string $statement)
3121
    {
3122
        if ($this->QBIgnore && isset($this->supportedIgnoreStatements[$statement])) {
704✔
3123
            return trim($this->supportedIgnoreStatements[$statement]) . ' ';
1✔
3124
        }
3125

3126
        return '';
703✔
3127
    }
3128

3129
    /**
3130
     * Escapes identifiers in WHERE and HAVING statements at execution time.
3131
     *
3132
     * Required so that aliases are tracked properly, regardless of whether
3133
     * where(), orWhere(), having(), orHaving are called prior to from(),
3134
     * join() and prefixTable is added only if needed.
3135
     *
3136
     * @param string $qbKey 'QBWhere' or 'QBHaving'
3137
     *
3138
     * @return string SQL statement
3139
     */
3140
    protected function compileWhereHaving(string $qbKey): string
3141
    {
3142
        if (! empty($this->{$qbKey})) {
893✔
3143
            foreach ($this->{$qbKey} as &$qbkey) {
799✔
3144
                // Is this condition already compiled?
3145
                if (is_string($qbkey)) {
799✔
3146
                    continue;
23✔
3147
                }
3148

3149
                if ($qbkey instanceof RawSql) {
799✔
3150
                    continue;
2✔
3151
                }
3152

3153
                if ($qbkey['condition'] instanceof RawSql) {
799✔
3154
                    $qbkey = $qbkey['condition'];
4✔
3155

3156
                    continue;
4✔
3157
                }
3158

3159
                if ($qbkey['escape'] === false) {
797✔
3160
                    $qbkey = $qbkey['condition'];
105✔
3161

3162
                    continue;
105✔
3163
                }
3164

3165
                // Split multiple conditions
3166
                $conditions = preg_split(
787✔
3167
                    '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i',
787✔
3168
                    $qbkey['condition'],
787✔
3169
                    -1,
787✔
3170
                    PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
787✔
3171
                );
787✔
3172

3173
                foreach ($conditions as &$condition) {
787✔
3174
                    $op = $this->getOperator($condition);
787✔
3175
                    if (
3176
                        $op === false
787✔
3177
                        || preg_match(
787✔
3178
                            '/^(\(?)(.*)(' . preg_quote($op, '/') . ')\s*(.*(?<!\)))?(\)?)$/i',
787✔
3179
                            $condition,
787✔
3180
                            $matches
787✔
3181
                        ) !== 1
787✔
3182
                    ) {
3183
                        continue;
684✔
3184
                    }
3185
                    // $matches = [
3186
                    //  0 => '(test <= foo)',   /* the whole thing */
3187
                    //  1 => '(',               /* optional */
3188
                    //  2 => 'test',            /* the field name */
3189
                    //  3 => ' <= ',            /* $op */
3190
                    //  4 => 'foo',                    /* optional, if $op is e.g. 'IS NULL' */
3191
                    //  5 => ')'                /* optional */
3192
                    // ];
3193

3194
                    if (isset($matches[4]) && $matches[4] !== '') {
787✔
3195
                        $protectIdentifiers = false;
750✔
3196
                        if (str_contains($matches[4], '.')) {
750✔
3197
                            $protectIdentifiers = true;
85✔
3198
                        }
3199

3200
                        if (! str_contains($matches[4], ':')) {
750✔
3201
                            $matches[4] = $this->db->protectIdentifiers(trim($matches[4]), false, $protectIdentifiers);
15✔
3202
                        }
3203

3204
                        $matches[4] = ' ' . $matches[4];
750✔
3205
                    }
3206

3207
                    $condition = $matches[1] . $this->db->protectIdentifiers(trim($matches[2]))
787✔
3208
                        . ' ' . trim($matches[3]) . $matches[4] . $matches[5];
787✔
3209
                }
3210

3211
                $qbkey = implode('', $conditions);
787✔
3212
            }
3213

3214
            return ($qbKey === 'QBHaving' ? "\nHAVING " : "\nWHERE ")
799✔
3215
                . implode("\n", $this->{$qbKey});
799✔
3216
        }
3217

3218
        return '';
875✔
3219
    }
3220

3221
    /**
3222
     * Escapes identifiers in GROUP BY statements at execution time.
3223
     *
3224
     * Required so that aliases are tracked properly, regardless of whether
3225
     * groupBy() is called prior to from(), join() and prefixTable is added
3226
     * only if needed.
3227
     */
3228
    protected function compileGroupBy(): string
3229
    {
3230
        if (! empty($this->QBGroupBy)) {
876✔
3231
            foreach ($this->QBGroupBy as &$groupBy) {
56✔
3232
                // Is it already compiled?
3233
                if (is_string($groupBy)) {
56✔
3234
                    continue;
2✔
3235
                }
3236

3237
                $groupBy = ($groupBy['escape'] === false || $this->isLiteral($groupBy['field']))
56✔
3238
                    ? $groupBy['field']
×
3239
                    : $this->db->protectIdentifiers($groupBy['field']);
56✔
3240
            }
3241

3242
            return "\nGROUP BY " . implode(', ', $this->QBGroupBy);
56✔
3243
        }
3244

3245
        return '';
850✔
3246
    }
3247

3248
    /**
3249
     * Escapes identifiers in ORDER BY statements at execution time.
3250
     *
3251
     * Required so that aliases are tracked properly, regardless of whether
3252
     * orderBy() is called prior to from(), join() and prefixTable is added
3253
     * only if needed.
3254
     */
3255
    protected function compileOrderBy(): string
3256
    {
3257
        if (is_array($this->QBOrderBy) && $this->QBOrderBy !== []) {
888✔
3258
            foreach ($this->QBOrderBy as &$orderBy) {
677✔
3259
                if ($orderBy['escape'] !== false && ! $this->isLiteral($orderBy['field'])) {
677✔
3260
                    $orderBy['field'] = $this->db->protectIdentifiers($orderBy['field']);
675✔
3261
                }
3262

3263
                $orderBy = $orderBy['field'] . $orderBy['direction'];
677✔
3264
            }
3265

3266
            return $this->QBOrderBy = "\nORDER BY " . implode(', ', $this->QBOrderBy);
677✔
3267
        }
3268

3269
        if (is_string($this->QBOrderBy)) {
863✔
3270
            return $this->QBOrderBy;
×
3271
        }
3272

3273
        return '';
863✔
3274
    }
3275

3276
    protected function unionInjection(string $sql): string
3277
    {
3278
        if ($this->QBUnion === []) {
876✔
3279
            return $sql;
876✔
3280
        }
3281

3282
        return 'SELECT * FROM (' . $sql . ') '
6✔
3283
            . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers('uwrp0') : 'uwrp0')
6✔
3284
            . implode("\n", $this->QBUnion);
6✔
3285
    }
3286

3287
    /**
3288
     * Takes an object as input and converts the class variables to array key/vals
3289
     *
3290
     * @param array|object $object
3291
     *
3292
     * @return array
3293
     */
3294
    protected function objectToArray($object)
3295
    {
3296
        if (! is_object($object)) {
701✔
3297
            return $object;
698✔
3298
        }
3299

3300
        if ($object instanceof RawSql) {
10✔
3301
            throw new InvalidArgumentException('RawSql "' . $object . '" cannot be used here.');
1✔
3302
        }
3303

3304
        $array = [];
9✔
3305

3306
        foreach (get_object_vars($object) as $key => $val) {
9✔
3307
            if ((! is_object($val) || $val instanceof RawSql) && ! is_array($val)) {
9✔
3308
                $array[$key] = $val;
9✔
3309
            }
3310
        }
3311

3312
        return $array;
9✔
3313
    }
3314

3315
    /**
3316
     * Takes an object as input and converts the class variables to array key/vals
3317
     *
3318
     * @param array|object $object
3319
     *
3320
     * @return array
3321
     */
3322
    protected function batchObjectToArray($object)
3323
    {
3324
        if (! is_object($object)) {
66✔
3325
            return $object;
66✔
3326
        }
3327

3328
        $array  = [];
×
3329
        $out    = get_object_vars($object);
×
3330
        $fields = array_keys($out);
×
3331

3332
        foreach ($fields as $val) {
×
3333
            $i = 0;
×
3334

3335
            foreach ($out[$val] as $data) {
×
3336
                $array[$i++][$val] = $data;
×
3337
            }
3338
        }
3339

3340
        return $array;
×
3341
    }
3342

3343
    /**
3344
     * Determines if a string represents a literal value or a field name
3345
     */
3346
    protected function isLiteral(string $str): bool
3347
    {
3348
        $str = trim($str);
705✔
3349

3350
        if ($str === ''
705✔
3351
            || ctype_digit($str)
705✔
3352
            || (string) (float) $str === $str
705✔
3353
            || in_array(strtoupper($str), ['TRUE', 'FALSE'], true)
705✔
3354
        ) {
3355
            return true;
×
3356
        }
3357

3358
        if ($this->isLiteralStr === []) {
705✔
3359
            $this->isLiteralStr = $this->db->escapeChar !== '"' ? ['"', "'"] : ["'"];
705✔
3360
        }
3361

3362
        return in_array($str[0], $this->isLiteralStr, true);
705✔
3363
    }
3364

3365
    /**
3366
     * Publicly-visible method to reset the QB values.
3367
     *
3368
     * @return $this
3369
     */
3370
    public function resetQuery()
3371
    {
3372
        $this->resetSelect();
1✔
3373
        $this->resetWrite();
1✔
3374

3375
        return $this;
1✔
3376
    }
3377

3378
    /**
3379
     * Resets the query builder values.  Called by the get() function
3380
     *
3381
     * @param array $qbResetItems An array of fields to reset
3382
     */
3383
    protected function resetRun(array $qbResetItems)
3384
    {
3385
        foreach ($qbResetItems as $item => $defaultValue) {
898✔
3386
            $this->{$item} = $defaultValue;
898✔
3387
        }
3388
    }
3389

3390
    /**
3391
     * Resets the query builder values.  Called by the get() function
3392
     */
3393
    protected function resetSelect()
3394
    {
3395
        $this->resetRun([
871✔
3396
            'QBSelect'   => [],
871✔
3397
            'QBJoin'     => [],
871✔
3398
            'QBWhere'    => [],
871✔
3399
            'QBGroupBy'  => [],
871✔
3400
            'QBHaving'   => [],
871✔
3401
            'QBOrderBy'  => [],
871✔
3402
            'QBNoEscape' => [],
871✔
3403
            'QBDistinct' => false,
871✔
3404
            'QBLimit'    => false,
871✔
3405
            'QBOffset'   => false,
871✔
3406
            'QBUnion'    => [],
871✔
3407
        ]);
871✔
3408

3409
        if (! empty($this->db)) {
871✔
3410
            $this->db->setAliasedTables([]);
871✔
3411
        }
3412

3413
        // Reset QBFrom part
3414
        if (! empty($this->QBFrom)) {
871✔
3415
            $this->from(array_shift($this->QBFrom), true);
871✔
3416
        }
3417
    }
3418

3419
    /**
3420
     * Resets the query builder "write" values.
3421
     *
3422
     * Called by the insert() update() insertBatch() updateBatch() and delete() functions
3423
     */
3424
    protected function resetWrite()
3425
    {
3426
        $this->resetRun([
708✔
3427
            'QBSet'     => [],
708✔
3428
            'QBJoin'    => [],
708✔
3429
            'QBWhere'   => [],
708✔
3430
            'QBOrderBy' => [],
708✔
3431
            'QBKeys'    => [],
708✔
3432
            'QBLimit'   => false,
708✔
3433
            'QBIgnore'  => false,
708✔
3434
            'QBOptions' => [],
708✔
3435
        ]);
708✔
3436
    }
3437

3438
    /**
3439
     * Tests whether the string has an SQL operator
3440
     */
3441
    protected function hasOperator(string $str): bool
3442
    {
3443
        return preg_match(
123✔
3444
            '/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i',
123✔
3445
            trim($str)
123✔
3446
        ) === 1;
123✔
3447
    }
3448

3449
    /**
3450
     * Returns the SQL string operator
3451
     *
3452
     * @return array|false|string
3453
     */
3454
    protected function getOperator(string $str, bool $list = false)
3455
    {
3456
        if ($this->pregOperators === []) {
796✔
3457
            $_les = $this->db->likeEscapeStr !== ''
796✔
3458
                ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/')
796✔
3459
                : '';
×
3460
            $this->pregOperators = [
796✔
3461
                '\s*(?:<|>|!)?=\s*', // =, <=, >=, !=
796✔
3462
                '\s*<>?\s*',         // <, <>
796✔
3463
                '\s*>\s*',           // >
796✔
3464
                '\s+IS NULL',             // IS NULL
796✔
3465
                '\s+IS NOT NULL',         // IS NOT NULL
796✔
3466
                '\s+EXISTS\s*\(.*\)',     // EXISTS (sql)
796✔
3467
                '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql)
796✔
3468
                '\s+BETWEEN\s+',          // BETWEEN value AND value
796✔
3469
                '\s+IN\s*\(.*\)',         // IN (list)
796✔
3470
                '\s+NOT IN\s*\(.*\)',     // NOT IN (list)
796✔
3471
                '\s+LIKE\s+\S.*(' . $_les . ')?',     // LIKE 'expr'[ ESCAPE '%s']
796✔
3472
                '\s+NOT LIKE\s+\S.*(' . $_les . ')?', // NOT LIKE 'expr'[ ESCAPE '%s']
796✔
3473
            ];
796✔
3474
        }
3475

3476
        return preg_match_all(
796✔
3477
            '/' . implode('|', $this->pregOperators) . '/i',
796✔
3478
            $str,
796✔
3479
            $match
796✔
3480
        ) >= 1 ? ($list ? $match[0] : $match[0][0]) : false;
796✔
3481
    }
3482

3483
    /**
3484
     * Returns the SQL string operator from where key
3485
     *
3486
     * @return false|list<string>
3487
     */
3488
    private function getOperatorFromWhereKey(string $whereKey)
3489
    {
3490
        $whereKey = trim($whereKey);
749✔
3491

3492
        $pregOperators = [
749✔
3493
            '\s*(?:<|>|!)?=',         // =, <=, >=, !=
749✔
3494
            '\s*<>?',                 // <, <>
749✔
3495
            '\s*>',                   // >
749✔
3496
            '\s+IS NULL',             // IS NULL
749✔
3497
            '\s+IS NOT NULL',         // IS NOT NULL
749✔
3498
            '\s+EXISTS\s*\(.*\)',     // EXISTS (sql)
749✔
3499
            '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS (sql)
749✔
3500
            '\s+BETWEEN\s+',          // BETWEEN value AND value
749✔
3501
            '\s+IN\s*\(.*\)',         // IN (list)
749✔
3502
            '\s+NOT IN\s*\(.*\)',     // NOT IN (list)
749✔
3503
            '\s+LIKE',                // LIKE
749✔
3504
            '\s+NOT LIKE',            // NOT LIKE
749✔
3505
        ];
749✔
3506

3507
        return preg_match_all(
749✔
3508
            '/' . implode('|', $pregOperators) . '/i',
749✔
3509
            $whereKey,
749✔
3510
            $match
749✔
3511
        ) >= 1 ? $match[0] : false;
749✔
3512
    }
3513

3514
    /**
3515
     * Stores a bind value after ensuring that it's unique.
3516
     * While it might be nicer to have named keys for our binds array
3517
     * with PHP 7+ we get a huge memory/performance gain with indexed
3518
     * arrays instead, so lets take advantage of that here.
3519
     *
3520
     * @param mixed $value
3521
     */
3522
    protected function setBind(string $key, $value = null, bool $escape = true): string
3523
    {
3524
        if (! array_key_exists($key, $this->binds)) {
796✔
3525
            $this->binds[$key] = [
796✔
3526
                $value,
796✔
3527
                $escape,
796✔
3528
            ];
796✔
3529

3530
            return $key;
796✔
3531
        }
3532

3533
        if (! array_key_exists($key, $this->bindsKeyCount)) {
43✔
3534
            $this->bindsKeyCount[$key] = 1;
43✔
3535
        }
3536

3537
        $count = $this->bindsKeyCount[$key]++;
43✔
3538

3539
        $this->binds[$key . '.' . $count] = [
43✔
3540
            $value,
43✔
3541
            $escape,
43✔
3542
        ];
43✔
3543

3544
        return $key . '.' . $count;
43✔
3545
    }
3546

3547
    /**
3548
     * Returns a clone of a Base Builder with reset query builder values.
3549
     *
3550
     * @return $this
3551
     *
3552
     * @deprecated
3553
     */
3554
    protected function cleanClone()
3555
    {
3556
        return (clone $this)->from([], true)->resetQuery();
×
3557
    }
3558

3559
    /**
3560
     * @param mixed $value
3561
     */
3562
    protected function isSubquery($value): bool
3563
    {
3564
        return $value instanceof BaseBuilder || $value instanceof Closure;
758✔
3565
    }
3566

3567
    /**
3568
     * @param BaseBuilder|Closure(BaseBuilder): BaseBuilder $builder
3569
     * @param bool                                          $wrapped Wrap the subquery in brackets
3570
     * @param string                                        $alias   Subquery alias
3571
     */
3572
    protected function buildSubquery($builder, bool $wrapped = false, string $alias = ''): string
3573
    {
3574
        if ($builder instanceof Closure) {
21✔
3575
            $builder($builder = $this->db->newQuery());
11✔
3576
        }
3577

3578
        if ($builder === $this) {
21✔
3579
            throw new DatabaseException('The subquery cannot be the same object as the main query object.');
1✔
3580
        }
3581

3582
        $subquery = strtr($builder->getCompiledSelect(false), "\n", ' ');
20✔
3583

3584
        if ($wrapped) {
20✔
3585
            $subquery = '(' . $subquery . ')';
20✔
3586
            $alias    = trim($alias);
20✔
3587

3588
            if ($alias !== '') {
20✔
3589
                $subquery .= ' ' . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers($alias) : $alias);
11✔
3590
            }
3591
        }
3592

3593
        return $subquery;
20✔
3594
    }
3595
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc