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

codeigniter4 / CodeIgniter4 / 8677009716

13 Apr 2024 11:45PM UTC coverage: 84.44% (-2.2%) from 86.607%
8677009716

push

github

web-flow
Merge pull request #8776 from kenjis/fix-findQualifiedNameFromPath-Cannot-declare-class-v3

fix: Cannot declare class CodeIgniter\Config\Services, because the name is already in use

0 of 3 new or added lines in 1 file covered. (0.0%)

478 existing lines in 72 files now uncovered.

20318 of 24062 relevant lines covered (84.44%)

188.23 hits per line

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

92.53
/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\Traits\ConditionalTrait;
20
use Config\Feature;
21
use InvalidArgumentException;
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
     * QB NO ESCAPE data
128
     *
129
     * @var array
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 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)) {
888✔
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;
888✔
317

318
        // If it contains `,`, it has multiple tables
319
        if (is_string($tableName) && ! str_contains($tableName, ',')) {
888✔
320
            $this->tableName = $tableName;  // @TODO remove alias if exists
886✔
321
        } else {
322
            $this->tableName = '';
16✔
323
        }
324

325
        $this->from($tableName);
888✔
326

327
        if ($options !== null && $options !== []) {
888✔
328
            foreach ($options as $key => $value) {
×
329
                if (property_exists($this, $key)) {
×
330
                    $this->{$key} = $value;
×
331
                }
332
            }
333
        }
334
    }
335

336
    /**
337
     * Returns the current database connection
338
     *
339
     * @return BaseConnection
340
     */
341
    public function db(): ConnectionInterface
342
    {
343
        return $this->db;
1✔
344
    }
345

346
    /**
347
     * Sets a test mode status.
348
     *
349
     * @return $this
350
     */
351
    public function testMode(bool $mode = true)
352
    {
353
        $this->testMode = $mode;
77✔
354

355
        return $this;
77✔
356
    }
357

358
    /**
359
     * Gets the name of the primary table.
360
     */
361
    public function getTable(): string
362
    {
363
        return $this->tableName;
5✔
364
    }
365

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

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

387
        return $this;
1✔
388
    }
389

390
    /**
391
     * Generates the SELECT portion of the query
392
     *
393
     * @param array|RawSql|string $select
394
     *
395
     * @return $this
396
     */
397
    public function select($select = '*', ?bool $escape = null)
398
    {
399
        // If the escape value was not set, we will base it on the global setting
400
        if (! is_bool($escape)) {
683✔
401
            $escape = $this->db->protectIdentifiers;
680✔
402
        }
403

404
        if ($select instanceof RawSql) {
683✔
405
            $this->QBSelect[] = $select;
1✔
406

407
            return $this;
1✔
408
        }
409

410
        if (is_string($select)) {
682✔
411
            $select = $escape === false ? [$select] : explode(',', $select);
681✔
412
        }
413

414
        foreach ($select as $val) {
682✔
415
            $val = trim($val);
682✔
416

417
            if ($val !== '') {
682✔
418
                $this->QBSelect[] = $val;
682✔
419

420
                /*
421
                 * When doing 'SELECT NULL as field_alias FROM table'
422
                 * null gets taken as a field, and therefore escaped
423
                 * with backticks.
424
                 * This prevents NULL being escaped
425
                 * @see https://github.com/codeigniter4/CodeIgniter4/issues/1169
426
                 */
427
                if (mb_stripos(trim($val), 'NULL') === 0) {
682✔
428
                    $escape = false;
×
429
                }
430

431
                $this->QBNoEscape[] = $escape;
682✔
432
            }
433
        }
434

435
        return $this;
682✔
436
    }
437

438
    /**
439
     * Generates a SELECT MAX(field) portion of a query
440
     *
441
     * @return $this
442
     */
443
    public function selectMax(string $select = '', string $alias = '')
444
    {
445
        return $this->maxMinAvgSum($select, $alias);
624✔
446
    }
447

448
    /**
449
     * Generates a SELECT MIN(field) portion of a query
450
     *
451
     * @return $this
452
     */
453
    public function selectMin(string $select = '', string $alias = '')
454
    {
455
        return $this->maxMinAvgSum($select, $alias, 'MIN');
4✔
456
    }
457

458
    /**
459
     * Generates a SELECT AVG(field) portion of a query
460
     *
461
     * @return $this
462
     */
463
    public function selectAvg(string $select = '', string $alias = '')
464
    {
465
        return $this->maxMinAvgSum($select, $alias, 'AVG');
4✔
466
    }
467

468
    /**
469
     * Generates a SELECT SUM(field) portion of a query
470
     *
471
     * @return $this
472
     */
473
    public function selectSum(string $select = '', string $alias = '')
474
    {
475
        return $this->maxMinAvgSum($select, $alias, 'SUM');
6✔
476
    }
477

478
    /**
479
     * Generates a SELECT COUNT(field) portion of a query
480
     *
481
     * @return $this
482
     */
483
    public function selectCount(string $select = '', string $alias = '')
484
    {
485
        return $this->maxMinAvgSum($select, $alias, 'COUNT');
5✔
486
    }
487

488
    /**
489
     * Adds a subquery to the selection
490
     */
491
    public function selectSubquery(BaseBuilder $subquery, string $as): self
492
    {
493
        $this->QBSelect[] = $this->buildSubquery($subquery, true, $as);
2✔
494

495
        return $this;
2✔
496
    }
497

498
    /**
499
     * SELECT [MAX|MIN|AVG|SUM|COUNT]()
500
     *
501
     * @used-by selectMax()
502
     * @used-by selectMin()
503
     * @used-by selectAvg()
504
     * @used-by selectSum()
505
     *
506
     * @return $this
507
     *
508
     * @throws DatabaseException
509
     * @throws DataException
510
     */
511
    protected function maxMinAvgSum(string $select = '', string $alias = '', string $type = 'MAX')
512
    {
513
        if ($select === '') {
634✔
514
            throw DataException::forEmptyInputGiven('Select');
1✔
515
        }
516

517
        if (str_contains($select, ',')) {
633✔
518
            throw DataException::forInvalidArgument('column name not separated by comma');
1✔
519
        }
520

521
        $type = strtoupper($type);
632✔
522

523
        if (! in_array($type, ['MAX', 'MIN', 'AVG', 'SUM', 'COUNT'], true)) {
632✔
524
            throw new DatabaseException('Invalid function type: ' . $type);
×
525
        }
526

527
        if ($alias === '') {
632✔
528
            $alias = $this->createAliasFromTable(trim($select));
627✔
529
        }
530

531
        $sql = $type . '(' . $this->db->protectIdentifiers(trim($select)) . ') AS ' . $this->db->escapeIdentifiers(trim($alias));
632✔
532

533
        $this->QBSelect[]   = $sql;
632✔
534
        $this->QBNoEscape[] = null;
632✔
535

536
        return $this;
632✔
537
    }
538

539
    /**
540
     * Determines the alias name based on the table
541
     */
542
    protected function createAliasFromTable(string $item): string
543
    {
544
        if (str_contains($item, '.')) {
627✔
545
            $item = explode('.', $item);
1✔
546

547
            return end($item);
1✔
548
        }
549

550
        return $item;
626✔
551
    }
552

553
    /**
554
     * Sets a flag which tells the query string compiler to add DISTINCT
555
     *
556
     * @return $this
557
     */
558
    public function distinct(bool $val = true)
559
    {
560
        $this->QBDistinct = $val;
626✔
561

562
        return $this;
626✔
563
    }
564

565
    /**
566
     * Generates the FROM portion of the query
567
     *
568
     * @param array|string $from
569
     *
570
     * @return $this
571
     */
572
    public function from($from, bool $overwrite = false): self
573
    {
574
        if ($overwrite === true) {
888✔
575
            $this->QBFrom = [];
826✔
576
            $this->db->setAliasedTables([]);
826✔
577
        }
578

579
        foreach ((array) $from as $table) {
888✔
580
            if (str_contains($table, ',')) {
888✔
581
                $this->from(explode(',', $table));
17✔
582
            } else {
583
                $table = trim($table);
888✔
584

585
                if ($table === '') {
888✔
586
                    continue;
14✔
587
                }
588

589
                $this->trackAliases($table);
888✔
590
                $this->QBFrom[] = $this->db->protectIdentifiers($table, true, null, false);
888✔
591
            }
592
        }
593

594
        return $this;
888✔
595
    }
596

597
    /**
598
     * @param BaseBuilder $from  Expected subquery
599
     * @param string      $alias Subquery alias
600
     *
601
     * @return $this
602
     */
603
    public function fromSubquery(BaseBuilder $from, string $alias): self
604
    {
605
        $table = $this->buildSubquery($from, true, $alias);
5✔
606

607
        $this->db->addTableAlias($alias);
4✔
608
        $this->QBFrom[] = $table;
4✔
609

610
        return $this;
4✔
611
    }
612

613
    /**
614
     * Generates the JOIN portion of the query
615
     *
616
     * @param RawSql|string $cond
617
     *
618
     * @return $this
619
     */
620
    public function join(string $table, $cond, string $type = '', ?bool $escape = null)
621
    {
622
        if ($type !== '') {
14✔
623
            $type = strtoupper(trim($type));
7✔
624

625
            if (! in_array($type, $this->joinTypes, true)) {
7✔
626
                $type = '';
×
627
            } else {
628
                $type .= ' ';
7✔
629
            }
630
        }
631

632
        // Extract any aliases that might exist. We use this information
633
        // in the protectIdentifiers to know whether to add a table prefix
634
        $this->trackAliases($table);
14✔
635

636
        if (! is_bool($escape)) {
14✔
637
            $escape = $this->db->protectIdentifiers;
14✔
638
        }
639

640
        // Do we want to escape the table name?
641
        if ($escape === true) {
14✔
642
            $table = $this->db->protectIdentifiers($table, true, null, false);
14✔
643
        }
644

645
        if ($cond instanceof RawSql) {
14✔
646
            $this->QBJoin[] = $type . 'JOIN ' . $table . ' ON ' . $cond;
1✔
647

648
            return $this;
1✔
649
        }
650

651
        if (! $this->hasOperator($cond)) {
13✔
652
            $cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')';
×
653
        } elseif ($escape === false) {
13✔
654
            $cond = ' ON ' . $cond;
×
655
        } else {
656
            // Split multiple conditions
657
            if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE)) {
13✔
658
                $conditions = [];
1✔
659
                $joints     = $joints[0];
1✔
660
                array_unshift($joints, ['', 0]);
1✔
661

662
                for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--) {
1✔
663
                    $joints[$i][1] += strlen($joints[$i][0]); // offset
1✔
664
                    $conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]);
1✔
665
                    $pos            = $joints[$i][1] - strlen($joints[$i][0]);
1✔
666
                    $joints[$i]     = $joints[$i][0];
1✔
667
                }
668
                ksort($conditions);
1✔
669
            } else {
670
                $conditions = [$cond];
12✔
671
                $joints     = [''];
12✔
672
            }
673

674
            $cond = ' ON ';
13✔
675

676
            foreach ($conditions as $i => $condition) {
13✔
677
                $operator = $this->getOperator($condition);
13✔
678

679
                $cond .= $joints[$i];
13✔
680
                $cond .= preg_match('/(\(*)?([\[\]\w\.\'-]+)' . preg_quote($operator, '/') . '(.*)/i', $condition, $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $condition;
13✔
681
            }
682
        }
683

684
        // Assemble the JOIN statement
685
        $this->QBJoin[] = $type . 'JOIN ' . $table . $cond;
13✔
686

687
        return $this;
13✔
688
    }
689

690
    /**
691
     * Generates the WHERE portion of the query.
692
     * Separates multiple calls with 'AND'.
693
     *
694
     * @param array|RawSql|string $key
695
     * @param mixed               $value
696
     *
697
     * @return $this
698
     */
699
    public function where($key, $value = null, ?bool $escape = null)
700
    {
701
        return $this->whereHaving('QBWhere', $key, $value, 'AND ', $escape);
729✔
702
    }
703

704
    /**
705
     * OR WHERE
706
     *
707
     * Generates the WHERE portion of the query.
708
     * Separates multiple calls with 'OR'.
709
     *
710
     * @param array|RawSql|string $key
711
     * @param mixed               $value
712
     *
713
     * @return $this
714
     */
715
    public function orWhere($key, $value = null, ?bool $escape = null)
716
    {
717
        return $this->whereHaving('QBWhere', $key, $value, 'OR ', $escape);
5✔
718
    }
719

720
    /**
721
     * @used-by where()
722
     * @used-by orWhere()
723
     * @used-by having()
724
     * @used-by orHaving()
725
     *
726
     * @param array|RawSql|string $key
727
     * @param mixed               $value
728
     *
729
     * @return $this
730
     */
731
    protected function whereHaving(string $qbKey, $key, $value = null, string $type = 'AND ', ?bool $escape = null)
732
    {
733
        $rawSqlOnly = false;
735✔
734

735
        if ($key instanceof RawSql) {
735✔
736
            if ($value === null) {
4✔
737
                $keyValue   = [(string) $key => $key];
1✔
738
                $rawSqlOnly = true;
1✔
739
            } else {
740
                $keyValue = [(string) $key => $value];
3✔
741
            }
742
        } elseif (! is_array($key)) {
731✔
743
            $keyValue = [$key => $value];
721✔
744
        } else {
745
            $keyValue = $key;
167✔
746
        }
747

748
        // If the escape value was not set will base it on the global setting
749
        if (! is_bool($escape)) {
735✔
750
            $escape = $this->db->protectIdentifiers;
731✔
751
        }
752

753
        foreach ($keyValue as $k => $v) {
735✔
754
            $prefix = empty($this->{$qbKey}) ? $this->groupGetType('') : $this->groupGetType($type);
735✔
755

756
            if ($rawSqlOnly === true) {
735✔
757
                $k  = '';
1✔
758
                $op = '';
1✔
759
            } elseif ($v !== null) {
734✔
760
                $op = $this->getOperatorFromWhereKey($k);
718✔
761

762
                if (! empty($op)) {
718✔
763
                    $k = trim($k);
49✔
764

765
                    end($op);
49✔
766
                    $op = trim(current($op));
49✔
767

768
                    // Does the key end with operator?
769
                    if (str_ends_with($k, $op)) {
49✔
770
                        $k  = rtrim(substr($k, 0, -strlen($op)));
49✔
771
                        $op = " {$op}";
49✔
772
                    } else {
773
                        $op = '';
×
774
                    }
775
                } else {
776
                    $op = ' =';
706✔
777
                }
778

779
                if ($this->isSubquery($v)) {
718✔
780
                    $v = $this->buildSubquery($v, true);
1✔
781
                } else {
782
                    $bind = $this->setBind($k, $v, $escape);
718✔
783
                    $v    = " :{$bind}:";
718✔
784
                }
785
            } elseif (! $this->hasOperator($k) && $qbKey !== 'QBHaving') {
108✔
786
                // value appears not to have been set, assign the test to IS NULL
787
                $op = ' IS NULL';
75✔
788
            } elseif (
789
                // The key ends with !=, =, <>, IS, IS NOT
790
                preg_match(
39✔
791
                    '/\s*(!?=|<>|IS(?:\s+NOT)?)\s*$/i',
39✔
792
                    $k,
39✔
793
                    $match,
39✔
794
                    PREG_OFFSET_CAPTURE
39✔
795
                )
39✔
796
            ) {
797
                $k  = substr($k, 0, $match[0][1]);
1✔
798
                $op = $match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL';
1✔
799
            } else {
800
                $op = '';
38✔
801
            }
802

803
            if ($v instanceof RawSql) {
735✔
804
                $this->{$qbKey}[] = [
1✔
805
                    'condition' => $v->with($prefix . $k . $op . $v),
1✔
806
                    'escape'    => $escape,
1✔
807
                ];
1✔
808
            } else {
809
                $this->{$qbKey}[] = [
734✔
810
                    'condition' => $prefix . $k . $op . $v,
734✔
811
                    'escape'    => $escape,
734✔
812
                ];
734✔
813
            }
814
        }
815

816
        return $this;
735✔
817
    }
818

819
    /**
820
     * Generates a WHERE field IN('item', 'item') SQL query,
821
     * joined with 'AND' if appropriate.
822
     *
823
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
824
     *
825
     * @return $this
826
     */
827
    public function whereIn(?string $key = null, $values = null, ?bool $escape = null)
828
    {
829
        return $this->_whereIn($key, $values, false, 'AND ', $escape);
62✔
830
    }
831

832
    /**
833
     * Generates a WHERE field IN('item', 'item') SQL query,
834
     * joined with 'OR' if appropriate.
835
     *
836
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
837
     *
838
     * @return $this
839
     */
840
    public function orWhereIn(?string $key = null, $values = null, ?bool $escape = null)
841
    {
842
        return $this->_whereIn($key, $values, false, 'OR ', $escape);
3✔
843
    }
844

845
    /**
846
     * Generates a WHERE field NOT IN('item', 'item') SQL query,
847
     * joined with 'AND' if appropriate.
848
     *
849
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
850
     *
851
     * @return $this
852
     */
853
    public function whereNotIn(?string $key = null, $values = null, ?bool $escape = null)
854
    {
855
        return $this->_whereIn($key, $values, true, 'AND ', $escape);
3✔
856
    }
857

858
    /**
859
     * Generates a WHERE field NOT IN('item', 'item') SQL query,
860
     * joined with 'OR' if appropriate.
861
     *
862
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
863
     *
864
     * @return $this
865
     */
866
    public function orWhereNotIn(?string $key = null, $values = null, ?bool $escape = null)
867
    {
868
        return $this->_whereIn($key, $values, true, 'OR ', $escape);
2✔
869
    }
870

871
    /**
872
     * Generates a HAVING field IN('item', 'item') SQL query,
873
     * joined with 'AND' if appropriate.
874
     *
875
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
876
     *
877
     * @return $this
878
     */
879
    public function havingIn(?string $key = null, $values = null, ?bool $escape = null)
880
    {
881
        return $this->_whereIn($key, $values, false, 'AND ', $escape, 'QBHaving');
6✔
882
    }
883

884
    /**
885
     * Generates a HAVING field IN('item', 'item') SQL query,
886
     * joined with 'OR' if appropriate.
887
     *
888
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
889
     *
890
     * @return $this
891
     */
892
    public function orHavingIn(?string $key = null, $values = null, ?bool $escape = null)
893
    {
894
        return $this->_whereIn($key, $values, false, 'OR ', $escape, 'QBHaving');
3✔
895
    }
896

897
    /**
898
     * Generates a HAVING field NOT IN('item', 'item') SQL query,
899
     * joined with 'AND' if appropriate.
900
     *
901
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
902
     *
903
     * @return $this
904
     */
905
    public function havingNotIn(?string $key = null, $values = null, ?bool $escape = null)
906
    {
907
        return $this->_whereIn($key, $values, true, 'AND ', $escape, 'QBHaving');
5✔
908
    }
909

910
    /**
911
     * Generates a HAVING field NOT IN('item', 'item') SQL query,
912
     * joined with 'OR' if appropriate.
913
     *
914
     * @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
915
     *
916
     * @return $this
917
     */
918
    public function orHavingNotIn(?string $key = null, $values = null, ?bool $escape = null)
919
    {
920
        return $this->_whereIn($key, $values, true, 'OR ', $escape, 'QBHaving');
3✔
921
    }
922

923
    /**
924
     * @used-by WhereIn()
925
     * @used-by orWhereIn()
926
     * @used-by whereNotIn()
927
     * @used-by orWhereNotIn()
928
     *
929
     * @param non-empty-string|null          $key
930
     * @param array|BaseBuilder|Closure|null $values The values searched on, or anonymous function with subquery
931
     *
932
     * @return $this
933
     *
934
     * @throws InvalidArgumentException
935
     */
936
    protected function _whereIn(?string $key = null, $values = null, bool $not = false, string $type = 'AND ', ?bool $escape = null, string $clause = 'QBWhere')
937
    {
938
        if ($key === null || $key === '') {
82✔
939
            throw new InvalidArgumentException(sprintf('%s() expects $key to be a non-empty string', debug_backtrace(0, 2)[1]['function']));
2✔
940
        }
941

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

946
        if (! is_bool($escape)) {
77✔
947
            $escape = $this->db->protectIdentifiers;
77✔
948
        }
949

950
        $ok = $key;
77✔
951

952
        if ($escape === true) {
77✔
953
            $key = $this->db->protectIdentifiers($key);
77✔
954
        }
955

956
        $not = ($not) ? ' NOT' : '';
77✔
957

958
        if ($this->isSubquery($values)) {
77✔
959
            $whereIn = $this->buildSubquery($values, true);
8✔
960
            $escape  = false;
8✔
961
        } else {
962
            $whereIn = array_values($values);
69✔
963
        }
964

965
        $ok = $this->setBind($ok, $whereIn, $escape);
77✔
966

967
        $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
77✔
968

969
        $whereIn = [
77✔
970
            'condition' => "{$prefix}{$key}{$not} IN :{$ok}:",
77✔
971
            'escape'    => false,
77✔
972
        ];
77✔
973

974
        $this->{$clause}[] = $whereIn;
77✔
975

976
        return $this;
77✔
977
    }
978

979
    /**
980
     * Generates a %LIKE% portion of the query.
981
     * Separates multiple calls with 'AND'.
982
     *
983
     * @param array|RawSql|string $field
984
     *
985
     * @return $this
986
     */
987
    public function like($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
988
    {
989
        return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch);
20✔
990
    }
991

992
    /**
993
     * Generates a NOT LIKE portion of the query.
994
     * Separates multiple calls with 'AND'.
995
     *
996
     * @param array|RawSql|string $field
997
     *
998
     * @return $this
999
     */
1000
    public function notLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1001
    {
1002
        return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch);
2✔
1003
    }
1004

1005
    /**
1006
     * Generates a %LIKE% portion of the query.
1007
     * Separates multiple calls with 'OR'.
1008
     *
1009
     * @param array|RawSql|string $field
1010
     *
1011
     * @return $this
1012
     */
1013
    public function orLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1014
    {
1015
        return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch);
2✔
1016
    }
1017

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

1031
    /**
1032
     * Generates a %LIKE% portion of the query.
1033
     * Separates multiple calls with 'AND'.
1034
     *
1035
     * @param array|RawSql|string $field
1036
     *
1037
     * @return $this
1038
     */
1039
    public function havingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1040
    {
1041
        return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch, 'QBHaving');
15✔
1042
    }
1043

1044
    /**
1045
     * Generates a NOT LIKE portion of the query.
1046
     * Separates multiple calls with 'AND'.
1047
     *
1048
     * @param array|RawSql|string $field
1049
     *
1050
     * @return $this
1051
     */
1052
    public function notHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1053
    {
1054
        return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch, 'QBHaving');
4✔
1055
    }
1056

1057
    /**
1058
     * Generates a %LIKE% portion of the query.
1059
     * Separates multiple calls with 'OR'.
1060
     *
1061
     * @param array|RawSql|string $field
1062
     *
1063
     * @return $this
1064
     */
1065
    public function orHavingLike($field, string $match = '', string $side = 'both', ?bool $escape = null, bool $insensitiveSearch = false)
1066
    {
1067
        return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch, 'QBHaving');
4✔
1068
    }
1069

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

1083
    /**
1084
     * @used-by like()
1085
     * @used-by orLike()
1086
     * @used-by notLike()
1087
     * @used-by orNotLike()
1088
     * @used-by havingLike()
1089
     * @used-by orHavingLike()
1090
     * @used-by notHavingLike()
1091
     * @used-by orNotHavingLike()
1092
     *
1093
     * @param array|RawSql|string $field
1094
     *
1095
     * @return $this
1096
     */
1097
    protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', ?bool $escape = null, bool $insensitiveSearch = false, string $clause = 'QBWhere')
1098
    {
1099
        $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
42✔
1100
        $side   = strtolower($side);
42✔
1101

1102
        if ($field instanceof RawSql) {
42✔
1103
            $k                 = (string) $field;
3✔
1104
            $v                 = $match;
3✔
1105
            $insensitiveSearch = false;
3✔
1106

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

1109
            if ($side === 'none') {
3✔
1110
                $bind = $this->setBind($field->getBindingKey(), $v, $escape);
×
1111
            } elseif ($side === 'before') {
3✔
1112
                $bind = $this->setBind($field->getBindingKey(), "%{$v}", $escape);
×
1113
            } elseif ($side === 'after') {
3✔
1114
                $bind = $this->setBind($field->getBindingKey(), "{$v}%", $escape);
×
1115
            } else {
1116
                $bind = $this->setBind($field->getBindingKey(), "%{$v}%", $escape);
3✔
1117
            }
1118

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

1121
            // some platforms require an escape sequence definition for LIKE wildcards
1122
            if ($escape === true && $this->db->likeEscapeStr !== '') {
3✔
1123
                $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar);
3✔
1124
            }
1125

1126
            $this->{$clause}[] = [
3✔
1127
                'condition' => $field->with($likeStatement),
3✔
1128
                'escape'    => $escape,
3✔
1129
            ];
3✔
1130

1131
            return $this;
3✔
1132
        }
1133

1134
        $keyValue = ! is_array($field) ? [$field => $match] : $field;
39✔
1135

1136
        foreach ($keyValue as $k => $v) {
39✔
1137
            if ($insensitiveSearch === true) {
39✔
1138
                $v = strtolower($v);
2✔
1139
            }
1140

1141
            $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
39✔
1142

1143
            if ($side === 'none') {
39✔
1144
                $bind = $this->setBind($k, $v, $escape);
1✔
1145
            } elseif ($side === 'before') {
38✔
1146
                $bind = $this->setBind($k, "%{$v}", $escape);
9✔
1147
            } elseif ($side === 'after') {
29✔
1148
                $bind = $this->setBind($k, "{$v}%", $escape);
5✔
1149
            } else {
1150
                $bind = $this->setBind($k, "%{$v}%", $escape);
24✔
1151
            }
1152

1153
            $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);
39✔
1154

1155
            // some platforms require an escape sequence definition for LIKE wildcards
1156
            if ($escape === true && $this->db->likeEscapeStr !== '') {
39✔
1157
                $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar);
39✔
1158
            }
1159

1160
            $this->{$clause}[] = [
39✔
1161
                'condition' => $likeStatement,
39✔
1162
                'escape'    => $escape,
39✔
1163
            ];
39✔
1164
        }
1165

1166
        return $this;
39✔
1167
    }
1168

1169
    /**
1170
     * Platform independent LIKE statement builder.
1171
     */
1172
    protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string
1173
    {
1174
        if ($insensitiveSearch === true) {
42✔
1175
            return "{$prefix} LOWER(" . $this->db->escapeIdentifiers($column) . ") {$not} LIKE :{$bind}:";
2✔
1176
        }
1177

1178
        return "{$prefix} {$column} {$not} LIKE :{$bind}:";
40✔
1179
    }
1180

1181
    /**
1182
     * Add UNION statement
1183
     *
1184
     * @param BaseBuilder|Closure $union
1185
     *
1186
     * @return $this
1187
     */
1188
    public function union($union)
1189
    {
1190
        return $this->addUnionStatement($union);
4✔
1191
    }
1192

1193
    /**
1194
     * Add UNION ALL statement
1195
     *
1196
     * @param BaseBuilder|Closure $union
1197
     *
1198
     * @return $this
1199
     */
1200
    public function unionAll($union)
1201
    {
1202
        return $this->addUnionStatement($union, true);
2✔
1203
    }
1204

1205
    /**
1206
     * @used-by union()
1207
     * @used-by unionAll()
1208
     *
1209
     * @param BaseBuilder|Closure $union
1210
     *
1211
     * @return $this
1212
     */
1213
    protected function addUnionStatement($union, bool $all = false)
1214
    {
1215
        $this->QBUnion[] = "\nUNION "
6✔
1216
            . ($all ? 'ALL ' : '')
6✔
1217
            . 'SELECT * FROM '
6✔
1218
            . $this->buildSubquery($union, true, 'uwrp' . (count($this->QBUnion) + 1));
6✔
1219

1220
        return $this;
6✔
1221
    }
1222

1223
    /**
1224
     * Starts a query group.
1225
     *
1226
     * @return $this
1227
     */
1228
    public function groupStart()
1229
    {
1230
        return $this->groupStartPrepare();
2✔
1231
    }
1232

1233
    /**
1234
     * Starts a query group, but ORs the group
1235
     *
1236
     * @return $this
1237
     */
1238
    public function orGroupStart()
1239
    {
1240
        return $this->groupStartPrepare('', 'OR ');
2✔
1241
    }
1242

1243
    /**
1244
     * Starts a query group, but NOTs the group
1245
     *
1246
     * @return $this
1247
     */
1248
    public function notGroupStart()
1249
    {
1250
        return $this->groupStartPrepare('NOT ');
2✔
1251
    }
1252

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

1263
    /**
1264
     * Ends a query group
1265
     *
1266
     * @return $this
1267
     */
1268
    public function groupEnd()
1269
    {
1270
        return $this->groupEndPrepare();
8✔
1271
    }
1272

1273
    /**
1274
     * Starts a query group for HAVING clause.
1275
     *
1276
     * @return $this
1277
     */
1278
    public function havingGroupStart()
1279
    {
1280
        return $this->groupStartPrepare('', 'AND ', 'QBHaving');
2✔
1281
    }
1282

1283
    /**
1284
     * Starts a query group for HAVING clause, but ORs the group.
1285
     *
1286
     * @return $this
1287
     */
1288
    public function orHavingGroupStart()
1289
    {
1290
        return $this->groupStartPrepare('', 'OR ', 'QBHaving');
2✔
1291
    }
1292

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

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

1313
    /**
1314
     * Ends a query group for HAVING clause.
1315
     *
1316
     * @return $this
1317
     */
1318
    public function havingGroupEnd()
1319
    {
1320
        return $this->groupEndPrepare('QBHaving');
8✔
1321
    }
1322

1323
    /**
1324
     * Prepate a query group start.
1325
     *
1326
     * @return $this
1327
     */
1328
    protected function groupStartPrepare(string $not = '', string $type = 'AND ', string $clause = 'QBWhere')
1329
    {
1330
        $type = $this->groupGetType($type);
16✔
1331

1332
        $this->QBWhereGroupStarted = true;
16✔
1333
        $prefix                    = empty($this->{$clause}) ? '' : $type;
16✔
1334
        $where                     = [
16✔
1335
            'condition' => $prefix . $not . str_repeat(' ', ++$this->QBWhereGroupCount) . ' (',
16✔
1336
            'escape'    => false,
16✔
1337
        ];
16✔
1338

1339
        $this->{$clause}[] = $where;
16✔
1340

1341
        return $this;
16✔
1342
    }
1343

1344
    /**
1345
     * Prepate a query group end.
1346
     *
1347
     * @return $this
1348
     */
1349
    protected function groupEndPrepare(string $clause = 'QBWhere')
1350
    {
1351
        $this->QBWhereGroupStarted = false;
16✔
1352
        $where                     = [
16✔
1353
            'condition' => str_repeat(' ', $this->QBWhereGroupCount--) . ')',
16✔
1354
            'escape'    => false,
16✔
1355
        ];
16✔
1356

1357
        $this->{$clause}[] = $where;
16✔
1358

1359
        return $this;
16✔
1360
    }
1361

1362
    /**
1363
     * @used-by groupStart()
1364
     * @used-by _like()
1365
     * @used-by whereHaving()
1366
     * @used-by _whereIn()
1367
     * @used-by havingGroupStart()
1368
     */
1369
    protected function groupGetType(string $type): string
1370
    {
1371
        if ($this->QBWhereGroupStarted) {
764✔
1372
            $type                      = '';
16✔
1373
            $this->QBWhereGroupStarted = false;
16✔
1374
        }
1375

1376
        return $type;
764✔
1377
    }
1378

1379
    /**
1380
     * @param array|string $by
1381
     *
1382
     * @return $this
1383
     */
1384
    public function groupBy($by, ?bool $escape = null)
1385
    {
1386
        if (! is_bool($escape)) {
56✔
1387
            $escape = $this->db->protectIdentifiers;
56✔
1388
        }
1389

1390
        if (is_string($by)) {
56✔
1391
            $by = ($escape === true) ? explode(',', $by) : [$by];
56✔
1392
        }
1393

1394
        foreach ($by as $val) {
56✔
1395
            $val = trim($val);
56✔
1396

1397
            if ($val !== '') {
56✔
1398
                $val = [
56✔
1399
                    'field'  => $val,
56✔
1400
                    'escape' => $escape,
56✔
1401
                ];
56✔
1402

1403
                $this->QBGroupBy[] = $val;
56✔
1404
            }
1405
        }
1406

1407
        return $this;
56✔
1408
    }
1409

1410
    /**
1411
     * Separates multiple calls with 'AND'.
1412
     *
1413
     * @param array|RawSql|string $key
1414
     * @param mixed               $value
1415
     *
1416
     * @return $this
1417
     */
1418
    public function having($key, $value = null, ?bool $escape = null)
1419
    {
1420
        return $this->whereHaving('QBHaving', $key, $value, 'AND ', $escape);
16✔
1421
    }
1422

1423
    /**
1424
     * Separates multiple calls with 'OR'.
1425
     *
1426
     * @param array|RawSql|string $key
1427
     * @param mixed               $value
1428
     *
1429
     * @return $this
1430
     */
1431
    public function orHaving($key, $value = null, ?bool $escape = null)
1432
    {
1433
        return $this->whereHaving('QBHaving', $key, $value, 'OR ', $escape);
2✔
1434
    }
1435

1436
    /**
1437
     * @param string $direction ASC, DESC or RANDOM
1438
     *
1439
     * @return $this
1440
     */
1441
    public function orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
1442
    {
1443
        $qbOrderBy = [];
647✔
1444
        if ($orderBy === '') {
647✔
1445
            return $this;
×
1446
        }
1447

1448
        $direction = strtoupper(trim($direction));
647✔
1449

1450
        if ($direction === 'RANDOM') {
647✔
1451
            $direction = '';
3✔
1452
            $orderBy   = ctype_digit($orderBy) ? sprintf($this->randomKeyword[1], $orderBy) : $this->randomKeyword[0];
3✔
1453
            $escape    = false;
3✔
1454
        } elseif ($direction !== '') {
645✔
1455
            $direction = in_array($direction, ['ASC', 'DESC'], true) ? ' ' . $direction : '';
645✔
1456
        }
1457

1458
        if (! is_bool($escape)) {
647✔
1459
            $escape = $this->db->protectIdentifiers;
645✔
1460
        }
1461

1462
        if ($escape === false) {
647✔
1463
            $qbOrderBy[] = [
3✔
1464
                'field'     => $orderBy,
3✔
1465
                'direction' => $direction,
3✔
1466
                'escape'    => false,
3✔
1467
            ];
3✔
1468
        } else {
1469
            $qbOrderBy = [];
645✔
1470

1471
            foreach (explode(',', $orderBy) as $field) {
645✔
1472
                $qbOrderBy[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE))
645✔
1473
                    ? [
×
1474
                        'field'     => ltrim(substr($field, 0, $match[0][1])),
×
1475
                        'direction' => ' ' . $match[1][0],
×
1476
                        'escape'    => true,
×
1477
                    ]
×
1478
                    : [
645✔
1479
                        'field'     => trim($field),
645✔
1480
                        'direction' => $direction,
645✔
1481
                        'escape'    => true,
645✔
1482
                    ];
645✔
1483
            }
1484
        }
1485

1486
        $this->QBOrderBy = array_merge($this->QBOrderBy, $qbOrderBy);
647✔
1487

1488
        return $this;
647✔
1489
    }
1490

1491
    /**
1492
     * @return $this
1493
     */
1494
    public function limit(?int $value = null, ?int $offset = 0)
1495
    {
1496
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
99✔
1497
        if ($limitZeroAsAll && $value === 0) {
99✔
1498
            $value = null;
12✔
1499
        }
1500

1501
        if ($value !== null) {
99✔
1502
            $this->QBLimit = $value;
88✔
1503
        }
1504

1505
        if ($offset !== null && $offset !== 0) {
99✔
1506
            $this->QBOffset = $offset;
9✔
1507
        }
1508

1509
        return $this;
99✔
1510
    }
1511

1512
    /**
1513
     * Sets the OFFSET value
1514
     *
1515
     * @return $this
1516
     */
1517
    public function offset(int $offset)
1518
    {
1519
        if ($offset !== 0) {
1✔
1520
            $this->QBOffset = $offset;
1✔
1521
        }
1522

1523
        return $this;
1✔
1524
    }
1525

1526
    /**
1527
     * Generates a platform-specific LIMIT clause.
1528
     */
1529
    protected function _limit(string $sql, bool $offsetIgnore = false): string
1530
    {
1531
        return $sql . ' LIMIT ' . ($offsetIgnore === false && $this->QBOffset ? $this->QBOffset . ', ' : '') . $this->QBLimit;
89✔
1532
    }
1533

1534
    /**
1535
     * Allows key/value pairs to be set for insert(), update() or replace().
1536
     *
1537
     * @param array|object|string $key    Field name, or an array of field/value pairs, or an object
1538
     * @param mixed               $value  Field value, if $key is a single field
1539
     * @param bool|null           $escape Whether to escape values
1540
     *
1541
     * @return $this
1542
     */
1543
    public function set($key, $value = '', ?bool $escape = null)
1544
    {
1545
        $key = $this->objectToArray($key);
662✔
1546

1547
        if (! is_array($key)) {
662✔
1548
            $key = [$key => $value];
106✔
1549
        }
1550

1551
        $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
662✔
1552

1553
        foreach ($key as $k => $v) {
662✔
1554
            if ($escape) {
662✔
1555
                $bind = $this->setBind($k, $v, $escape);
661✔
1556

1557
                $this->QBSet[$this->db->protectIdentifiers($k, false)] = ":{$bind}:";
661✔
1558
            } else {
1559
                $this->QBSet[$this->db->protectIdentifiers($k, false)] = $v;
10✔
1560
            }
1561
        }
1562

1563
        return $this;
662✔
1564
    }
1565

1566
    /**
1567
     * Returns the previously set() data, alternatively resetting it if needed.
1568
     */
1569
    public function getSetData(bool $clean = false): array
1570
    {
1571
        $data = $this->QBSet;
×
1572

1573
        if ($clean) {
×
1574
            $this->QBSet = [];
×
1575
        }
1576

1577
        return $data;
×
1578
    }
1579

1580
    /**
1581
     * Compiles a SELECT query string and returns the sql.
1582
     */
1583
    public function getCompiledSelect(bool $reset = true): string
1584
    {
1585
        $select = $this->compileSelect();
166✔
1586

1587
        if ($reset === true) {
166✔
1588
            $this->resetSelect();
164✔
1589
        }
1590

1591
        return $this->compileFinalQuery($select);
166✔
1592
    }
1593

1594
    /**
1595
     * Returns a finalized, compiled query string with the bindings
1596
     * inserted and prefixes swapped out.
1597
     */
1598
    protected function compileFinalQuery(string $sql): string
1599
    {
1600
        $query = new Query($this->db);
191✔
1601
        $query->setQuery($sql, $this->binds, false);
191✔
1602

1603
        if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
191✔
1604
            $query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
×
1605
        }
1606

1607
        return $query->getQuery();
191✔
1608
    }
1609

1610
    /**
1611
     * Compiles the select statement based on the other functions called
1612
     * and runs the query
1613
     *
1614
     * @return false|ResultInterface
1615
     */
1616
    public function get(?int $limit = null, int $offset = 0, bool $reset = true)
1617
    {
1618
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
673✔
1619
        if ($limitZeroAsAll && $limit === 0) {
673✔
1620
            $limit = null;
1✔
1621
        }
1622

1623
        if ($limit !== null) {
673✔
1624
            $this->limit($limit, $offset);
3✔
1625
        }
1626

1627
        $result = $this->testMode
673✔
1628
            ? $this->getCompiledSelect($reset)
2✔
1629
            : $this->db->query($this->compileSelect(), $this->binds, false);
671✔
1630

1631
        if ($reset === true) {
673✔
1632
            $this->resetSelect();
673✔
1633

1634
            // Clear our binds so we don't eat up memory
1635
            $this->binds = [];
673✔
1636
        }
1637

1638
        return $result;
673✔
1639
    }
1640

1641
    /**
1642
     * Generates a platform-specific query string that counts all records in
1643
     * the particular table
1644
     *
1645
     * @return int|string
1646
     */
1647
    public function countAll(bool $reset = true)
1648
    {
1649
        $table = $this->QBFrom[0];
6✔
1650

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

1654
        if ($this->testMode) {
6✔
1655
            return $sql;
1✔
1656
        }
1657

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

1660
        if (empty($query->getResult())) {
5✔
1661
            return 0;
×
1662
        }
1663

1664
        $query = $query->getRow();
5✔
1665

1666
        if ($reset === true) {
5✔
1667
            $this->resetSelect();
5✔
1668
        }
1669

1670
        return (int) $query->numrows;
5✔
1671
    }
1672

1673
    /**
1674
     * Generates a platform-specific query string that counts all records
1675
     * returned by an Query Builder query.
1676
     *
1677
     * @return int|string
1678
     */
1679
    public function countAllResults(bool $reset = true)
1680
    {
1681
        // ORDER BY usage is often problematic here (most notably
1682
        // on Microsoft SQL Server) and ultimately unnecessary
1683
        // for selecting COUNT(*) ...
1684
        $orderBy = [];
158✔
1685

1686
        if (! empty($this->QBOrderBy)) {
158✔
1687
            $orderBy = $this->QBOrderBy;
×
1688

1689
            $this->QBOrderBy = null;
×
1690
        }
1691

1692
        // We cannot use a LIMIT when getting the single row COUNT(*) result
1693
        $limit = $this->QBLimit;
158✔
1694

1695
        $this->QBLimit = false;
158✔
1696

1697
        if ($this->QBDistinct === true || ! empty($this->QBGroupBy)) {
158✔
1698
            // We need to backup the original SELECT in case DBPrefix is used
1699
            $select = $this->QBSelect;
4✔
1700
            $sql    = $this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . $this->compileSelect() . "\n) CI_count_all_results";
4✔
1701

1702
            // Restore SELECT part
1703
            $this->QBSelect = $select;
4✔
1704
            unset($select);
4✔
1705
        } else {
1706
            $sql = $this->compileSelect($this->countString . $this->db->protectIdentifiers('numrows'));
154✔
1707
        }
1708

1709
        if ($this->testMode) {
158✔
1710
            return $sql;
9✔
1711
        }
1712

1713
        $result = $this->db->query($sql, $this->binds, false);
153✔
1714

1715
        if ($reset === true) {
153✔
1716
            $this->resetSelect();
144✔
1717
        } elseif (! isset($this->QBOrderBy)) {
15✔
1718
            $this->QBOrderBy = $orderBy;
×
1719
        }
1720

1721
        // Restore the LIMIT setting
1722
        $this->QBLimit = $limit;
153✔
1723

1724
        $row = ! $result instanceof ResultInterface ? null : $result->getRow();
153✔
1725

1726
        if (empty($row)) {
153✔
1727
            return 0;
×
1728
        }
1729

1730
        return (int) $row->numrows;
153✔
1731
    }
1732

1733
    /**
1734
     * Compiles the set conditions and returns the sql statement
1735
     *
1736
     * @return array
1737
     */
1738
    public function getCompiledQBWhere()
1739
    {
1740
        return $this->QBWhere;
64✔
1741
    }
1742

1743
    /**
1744
     * Allows the where clause, limit and offset to be added directly
1745
     *
1746
     * @param array|string $where
1747
     *
1748
     * @return ResultInterface
1749
     */
1750
    public function getWhere($where = null, ?int $limit = null, ?int $offset = 0, bool $reset = true)
1751
    {
1752
        if ($where !== null) {
17✔
1753
            $this->where($where);
16✔
1754
        }
1755

1756
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
17✔
1757
        if ($limitZeroAsAll && $limit === 0) {
17✔
1758
            $limit = null;
×
1759
        }
1760

1761
        if ($limit !== null) {
17✔
1762
            $this->limit($limit, $offset);
3✔
1763
        }
1764

1765
        $result = $this->testMode
17✔
1766
            ? $this->getCompiledSelect($reset)
4✔
1767
            : $this->db->query($this->compileSelect(), $this->binds, false);
13✔
1768

1769
        if ($reset === true) {
17✔
1770
            $this->resetSelect();
17✔
1771

1772
            // Clear our binds so we don't eat up memory
1773
            $this->binds = [];
17✔
1774
        }
1775

1776
        return $result;
17✔
1777
    }
1778

1779
    /**
1780
     * Compiles batch insert/update/upsert strings and runs the queries
1781
     *
1782
     * @param '_deleteBatch'|'_insertBatch'|'_updateBatch'|'_upsertBatch' $renderMethod
1783
     *
1784
     * @return false|int|list<string> Number of rows inserted or FALSE on failure, SQL array when testMode
1785
     *
1786
     * @throws DatabaseException
1787
     */
1788
    protected function batchExecute(string $renderMethod, int $batchSize = 100)
1789
    {
1790
        if (empty($this->QBSet)) {
69✔
1791
            if ($this->db->DBDebug) {
5✔
1792
                throw new DatabaseException(trim($renderMethod, '_') . '() has no data.');
5✔
1793
            }
1794

UNCOV
1795
            return false; // @codeCoverageIgnore
×
1796
        }
1797

1798
        $table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);
64✔
1799

1800
        $affectedRows = 0;
64✔
1801
        $savedSQL     = [];
64✔
1802
        $cnt          = count($this->QBSet);
64✔
1803

1804
        // batch size 0 for unlimited
1805
        if ($batchSize === 0) {
64✔
1806
            $batchSize = $cnt;
×
1807
        }
1808

1809
        for ($i = 0, $total = $cnt; $i < $total; $i += $batchSize) {
64✔
1810
            $QBSet = array_slice($this->QBSet, $i, $batchSize);
64✔
1811

1812
            $sql = $this->{$renderMethod}($table, $this->QBKeys, $QBSet);
64✔
1813

1814
            if ($sql === '') {
61✔
UNCOV
1815
                return false; // @codeCoverageIgnore
×
1816
            }
1817

1818
            if ($this->testMode) {
61✔
1819
                $savedSQL[] = $sql;
3✔
1820
            } else {
1821
                $this->db->query($sql, null, false);
58✔
1822
                $affectedRows += $this->db->affectedRows();
57✔
1823
            }
1824
        }
1825

1826
        if (! $this->testMode) {
60✔
1827
            $this->resetWrite();
57✔
1828
        }
1829

1830
        return $this->testMode ? $savedSQL : $affectedRows;
60✔
1831
    }
1832

1833
    /**
1834
     * Allows a row or multiple rows to be set for batch inserts/upserts/updates
1835
     *
1836
     * @param array|object $set
1837
     * @param string       $alias alias for sql table
1838
     *
1839
     * @return $this|null
1840
     */
1841
    public function setData($set, ?bool $escape = null, string $alias = '')
1842
    {
1843
        if (empty($set)) {
64✔
1844
            if ($this->db->DBDebug) {
×
1845
                throw new DatabaseException('setData() has no data.');
×
1846
            }
1847

UNCOV
1848
            return null; // @codeCoverageIgnore
×
1849
        }
1850

1851
        $this->setAlias($alias);
64✔
1852

1853
        // this allows to set just one row at a time
1854
        if (is_object($set) || (! is_array(current($set)) && ! is_object(current($set)))) {
64✔
1855
            $set = [$set];
11✔
1856
        }
1857

1858
        $set = $this->batchObjectToArray($set);
64✔
1859

1860
        $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;
64✔
1861

1862
        $keys = array_keys($this->objectToArray(current($set)));
64✔
1863
        sort($keys);
64✔
1864

1865
        foreach ($set as $row) {
64✔
1866
            $row = $this->objectToArray($row);
64✔
1867
            if (array_diff($keys, array_keys($row)) !== [] || array_diff(array_keys($row), $keys) !== []) {
64✔
1868
                // batchExecute() function returns an error on an empty array
1869
                $this->QBSet[] = [];
×
1870

1871
                return null;
×
1872
            }
1873

1874
            ksort($row); // puts $row in the same order as our keys
64✔
1875

1876
            $clean = [];
64✔
1877

1878
            foreach ($row as $rowValue) {
64✔
1879
                $clean[] = $escape ? $this->db->escape($rowValue) : $rowValue;
64✔
1880
            }
1881

1882
            $row = $clean;
64✔
1883

1884
            $this->QBSet[] = $row;
64✔
1885
        }
1886

1887
        foreach ($keys as $k) {
64✔
1888
            $k = $this->db->protectIdentifiers($k, false);
64✔
1889

1890
            if (! in_array($k, $this->QBKeys, true)) {
64✔
1891
                $this->QBKeys[] = $k;
64✔
1892
            }
1893
        }
1894

1895
        return $this;
64✔
1896
    }
1897

1898
    /**
1899
     * Compiles an upsert query and returns the sql
1900
     *
1901
     * @return string
1902
     *
1903
     * @throws DatabaseException
1904
     */
1905
    public function getCompiledUpsert()
1906
    {
1907
        [$currentTestMode, $this->testMode] = [$this->testMode, true];
3✔
1908

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

1911
        $this->testMode = $currentTestMode;
3✔
1912

1913
        return $this->compileFinalQuery($sql);
3✔
1914
    }
1915

1916
    /**
1917
     * Converts call to batchUpsert
1918
     *
1919
     * @param array|object|null $set
1920
     *
1921
     * @return false|int|list<string> Number of affected rows or FALSE on failure, SQL array when testMode
1922
     *
1923
     * @throws DatabaseException
1924
     */
1925
    public function upsert($set = null, ?bool $escape = null)
1926
    {
1927
        // if set() has been used merge QBSet with binds and then setData()
1928
        if ($set === null && ! is_array(current($this->QBSet))) {
10✔
1929
            $set = [];
2✔
1930

1931
            foreach ($this->QBSet as $field => $value) {
2✔
1932
                $k = trim($field, $this->db->escapeChar);
2✔
1933
                // use binds if available else use QBSet value but with RawSql to avoid escape
1934
                $set[$k] = isset($this->binds[$k]) ? $this->binds[$k][0] : new RawSql($value);
2✔
1935
            }
1936

1937
            $this->binds = [];
2✔
1938

1939
            $this->resetRun([
2✔
1940
                'QBSet'  => [],
2✔
1941
                'QBKeys' => [],
2✔
1942
            ]);
2✔
1943

1944
            $this->setData($set, true); // unescaped items are RawSql now
2✔
1945
        } elseif ($set !== null) {
8✔
1946
            $this->setData($set, $escape);
7✔
1947
        } // else setData() has already been used and we need to do nothing
1948

1949
        return $this->batchExecute('_upsertBatch');
10✔
1950
    }
1951

1952
    /**
1953
     * Compiles batch upsert strings and runs the queries
1954
     *
1955
     * @param array|object|null $set a dataset
1956
     *
1957
     * @return false|int|list<string> Number of affected rows or FALSE on failure, SQL array when testMode
1958
     *
1959
     * @throws DatabaseException
1960
     */
1961
    public function upsertBatch($set = null, ?bool $escape = null, int $batchSize = 100)
1962
    {
1963
        if (isset($this->QBOptions['setQueryAsData'])) {
11✔
1964
            $sql = $this->_upsertBatch($this->QBFrom[0], $this->QBKeys, []);
1✔
1965

1966
            if ($sql === '') {
1✔
UNCOV
1967
                return false; // @codeCoverageIgnore
×
1968
            }
1969

1970
            if ($this->testMode === false) {
1✔
1971
                $this->db->query($sql, null, false);
1✔
1972
            }
1973

1974
            $this->resetWrite();
1✔
1975

1976
            return $this->testMode ? $sql : $this->db->affectedRows();
1✔
1977
        }
1978

1979
        if ($set !== null) {
10✔
1980
            $this->setData($set, $escape);
8✔
1981
        }
1982

1983
        return $this->batchExecute('_upsertBatch', $batchSize);
10✔
1984
    }
1985

1986
    /**
1987
     * Generates a platform-specific upsertBatch string from the supplied data
1988
     *
1989
     * @used-by batchExecute()
1990
     *
1991
     * @param string                 $table  Protected table name
1992
     * @param list<string>           $keys   QBKeys
1993
     * @param list<list<int|string>> $values QBSet
1994
     */
1995
    protected function _upsertBatch(string $table, array $keys, array $values): string
1996
    {
1997
        $sql = $this->QBOptions['sql'] ?? '';
18✔
1998

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

2003
            $sql = 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ")\n{:_table_:}ON DUPLICATE KEY UPDATE\n" . implode(
18✔
2004
                ",\n",
18✔
2005
                array_map(
18✔
2006
                    static fn ($key, $value) => $table . '.' . $key . ($value instanceof RawSql ?
18✔
2007
                        ' = ' . $value :
2✔
2008
                        ' = VALUES(' . $value . ')'),
18✔
2009
                    array_keys($updateFields),
18✔
2010
                    $updateFields
18✔
2011
                )
18✔
2012
            );
18✔
2013

2014
            $this->QBOptions['sql'] = $sql;
18✔
2015
        }
2016

2017
        if (isset($this->QBOptions['setQueryAsData'])) {
18✔
2018
            $data = $this->QBOptions['setQueryAsData'] . "\n";
1✔
2019
        } else {
2020
            $data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
17✔
2021
        }
2022

2023
        return str_replace('{:_table_:}', $data, $sql);
18✔
2024
    }
2025

2026
    /**
2027
     * Set table alias for dataset pseudo table.
2028
     */
2029
    private function setAlias(string $alias): BaseBuilder
2030
    {
2031
        if ($alias !== '') {
64✔
2032
            $this->db->addTableAlias($alias);
7✔
2033
            $this->QBOptions['alias'] = $this->db->protectIdentifiers($alias);
7✔
2034
        }
2035

2036
        return $this;
64✔
2037
    }
2038

2039
    /**
2040
     * Sets update fields for upsert, update
2041
     *
2042
     * @param list<RawSql>|list<string>|string $set
2043
     * @param bool                             $addToDefault adds update fields to the default ones
2044
     * @param array|null                       $ignore       ignores items in set
2045
     *
2046
     * @return $this
2047
     */
2048
    public function updateFields($set, bool $addToDefault = false, ?array $ignore = null)
2049
    {
2050
        if (! empty($set)) {
36✔
2051
            if (! is_array($set)) {
36✔
2052
                $set = explode(',', $set);
5✔
2053
            }
2054

2055
            foreach ($set as $key => $value) {
36✔
2056
                if (! ($value instanceof RawSql)) {
36✔
2057
                    $value = $this->db->protectIdentifiers($value);
36✔
2058
                }
2059

2060
                if (is_numeric($key)) {
36✔
2061
                    $key = $value;
36✔
2062
                }
2063

2064
                if ($ignore === null || ! in_array($key, $ignore, true)) {
36✔
2065
                    if ($addToDefault) {
36✔
2066
                        $this->QBOptions['updateFieldsAdditional'][$this->db->protectIdentifiers($key)] = $value;
3✔
2067
                    } else {
2068
                        $this->QBOptions['updateFields'][$this->db->protectIdentifiers($key)] = $value;
36✔
2069
                    }
2070
                }
2071
            }
2072

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

2076
                unset($this->QBOptions['updateFieldsAdditional']);
3✔
2077
            }
2078
        }
2079

2080
        return $this;
36✔
2081
    }
2082

2083
    /**
2084
     * Sets constraints for batch upsert, update
2085
     *
2086
     * @param array|RawSql|string $set a string of columns, key value pairs, or RawSql
2087
     *
2088
     * @return $this
2089
     */
2090
    public function onConstraint($set)
2091
    {
2092
        if (! empty($set)) {
45✔
2093
            if (is_string($set)) {
42✔
2094
                $set = explode(',', $set);
26✔
2095

2096
                $set = array_map(static fn ($key) => trim($key), $set);
26✔
2097
            }
2098

2099
            if ($set instanceof RawSql) {
42✔
2100
                $set = [$set];
2✔
2101
            }
2102

2103
            foreach ($set as $key => $value) {
42✔
2104
                if (! ($value instanceof RawSql)) {
42✔
2105
                    $value = $this->db->protectIdentifiers($value);
39✔
2106
                }
2107

2108
                if (is_string($key)) {
42✔
2109
                    $key = $this->db->protectIdentifiers($key);
3✔
2110
                }
2111

2112
                $this->QBOptions['constraints'][$key] = $value;
42✔
2113
            }
2114
        }
2115

2116
        return $this;
45✔
2117
    }
2118

2119
    /**
2120
     * Sets data source as a query for insertBatch()/updateBatch()/upsertBatch()/deleteBatch()
2121
     *
2122
     * @param BaseBuilder|RawSql $query
2123
     * @param array|string|null  $columns an array or comma delimited string of columns
2124
     */
2125
    public function setQueryAsData($query, ?string $alias = null, $columns = null): BaseBuilder
2126
    {
2127
        if (is_string($query)) {
5✔
2128
            throw new InvalidArgumentException('$query parameter must be BaseBuilder or RawSql class.');
×
2129
        }
2130

2131
        if ($query instanceof BaseBuilder) {
5✔
2132
            $query = $query->getCompiledSelect();
4✔
2133
        } elseif ($query instanceof RawSql) {
1✔
2134
            $query = $query->__toString();
1✔
2135
        }
2136

2137
        if (is_string($query)) {
5✔
2138
            if ($columns !== null && is_string($columns)) {
5✔
2139
                $columns = explode(',', $columns);
1✔
2140
                $columns = array_map(static fn ($key) => trim($key), $columns);
1✔
2141
            }
2142

2143
            $columns = (array) $columns;
5✔
2144

2145
            if ($columns === []) {
5✔
2146
                $columns = $this->fieldsFromQuery($query);
4✔
2147
            }
2148

2149
            if ($alias !== null) {
5✔
2150
                $this->setAlias($alias);
1✔
2151
            }
2152

2153
            foreach ($columns as $key => $value) {
5✔
2154
                $columns[$key] = $this->db->escapeChar . $value . $this->db->escapeChar;
5✔
2155
            }
2156

2157
            $this->QBOptions['setQueryAsData'] = $query;
5✔
2158
            $this->QBKeys                      = $columns;
5✔
2159
            $this->QBSet                       = [];
5✔
2160
        }
2161

2162
        return $this;
5✔
2163
    }
2164

2165
    /**
2166
     * Gets column names from a select query
2167
     */
2168
    protected function fieldsFromQuery(string $sql): array
2169
    {
2170
        return $this->db->query('SELECT * FROM (' . $sql . ') _u_ LIMIT 1')->getFieldNames();
4✔
2171
    }
2172

2173
    /**
2174
     * Converts value array of array to array of strings
2175
     */
2176
    protected function formatValues(array $values): array
2177
    {
2178
        return array_map(static fn ($index) => '(' . implode(',', $index) . ')', $values);
42✔
2179
    }
2180

2181
    /**
2182
     * Compiles batch insert strings and runs the queries
2183
     *
2184
     * @param array|object|null $set a dataset
2185
     *
2186
     * @return false|int|list<string> Number of rows inserted or FALSE on failure, SQL array when testMode
2187
     */
2188
    public function insertBatch($set = null, ?bool $escape = null, int $batchSize = 100)
2189
    {
2190
        if (isset($this->QBOptions['setQueryAsData'])) {
27✔
2191
            $sql = $this->_insertBatch($this->QBFrom[0], $this->QBKeys, []);
2✔
2192

2193
            if ($sql === '') {
2✔
UNCOV
2194
                return false; // @codeCoverageIgnore
×
2195
            }
2196

2197
            if ($this->testMode === false) {
2✔
2198
                $this->db->query($sql, null, false);
2✔
2199
            }
2200

2201
            $this->resetWrite();
2✔
2202

2203
            return $this->testMode ? $sql : $this->db->affectedRows();
2✔
2204
        }
2205

2206
        if ($set !== null && $set !== []) {
27✔
2207
            $this->setData($set, $escape);
24✔
2208
        }
2209

2210
        return $this->batchExecute('_insertBatch', $batchSize);
27✔
2211
    }
2212

2213
    /**
2214
     * Generates a platform-specific insert string from the supplied data.
2215
     *
2216
     * @used-by batchExecute()
2217
     *
2218
     * @param string                 $table  Protected table name
2219
     * @param list<string>           $keys   QBKeys
2220
     * @param list<list<int|string>> $values QBSet
2221
     */
2222
    protected function _insertBatch(string $table, array $keys, array $values): string
2223
    {
2224
        $sql = $this->QBOptions['sql'] ?? '';
25✔
2225

2226
        // if this is the first iteration of batch then we need to build skeleton sql
2227
        if ($sql === '') {
25✔
2228
            $sql = 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table
25✔
2229
                . ' (' . implode(', ', $keys) . ")\n{:_table_:}";
25✔
2230

2231
            $this->QBOptions['sql'] = $sql;
25✔
2232
        }
2233

2234
        if (isset($this->QBOptions['setQueryAsData'])) {
25✔
2235
            $data = $this->QBOptions['setQueryAsData'];
2✔
2236
        } else {
2237
            $data = 'VALUES ' . implode(', ', $this->formatValues($values));
25✔
2238
        }
2239

2240
        return str_replace('{:_table_:}', $data, $sql);
25✔
2241
    }
2242

2243
    /**
2244
     * Allows key/value pairs to be set for batch inserts
2245
     *
2246
     * @param mixed $key
2247
     *
2248
     * @return $this|null
2249
     *
2250
     * @deprecated
2251
     */
2252
    public function setInsertBatch($key, string $value = '', ?bool $escape = null)
2253
    {
2254
        if (! is_array($key)) {
1✔
UNCOV
2255
            $key = [[$key => $value]];
×
2256
        }
2257

2258
        return $this->setData($key, $escape);
1✔
2259
    }
2260

2261
    /**
2262
     * Compiles an insert query and returns the sql
2263
     *
2264
     * @return bool|string
2265
     *
2266
     * @throws DatabaseException
2267
     */
2268
    public function getCompiledInsert(bool $reset = true)
2269
    {
2270
        if ($this->validateInsert() === false) {
6✔
2271
            return false;
×
2272
        }
2273

2274
        $sql = $this->_insert(
6✔
2275
            $this->db->protectIdentifiers(
6✔
2276
                $this->removeAlias($this->QBFrom[0]),
6✔
2277
                true,
6✔
2278
                null,
6✔
2279
                false
6✔
2280
            ),
6✔
2281
            array_keys($this->QBSet),
6✔
2282
            array_values($this->QBSet)
6✔
2283
        );
6✔
2284

2285
        if ($reset === true) {
6✔
2286
            $this->resetWrite();
6✔
2287
        }
2288

2289
        return $this->compileFinalQuery($sql);
6✔
2290
    }
2291

2292
    /**
2293
     * Compiles an insert string and runs the query
2294
     *
2295
     * @param array|object|null $set
2296
     *
2297
     * @return BaseResult|bool|Query
2298
     *
2299
     * @throws DatabaseException
2300
     */
2301
    public function insert($set = null, ?bool $escape = null)
2302
    {
2303
        if ($set !== null) {
649✔
2304
            $this->set($set, '', $escape);
617✔
2305
        }
2306

2307
        if ($this->validateInsert() === false) {
649✔
2308
            return false;
×
2309
        }
2310

2311
        $sql = $this->_insert(
648✔
2312
            $this->db->protectIdentifiers(
648✔
2313
                $this->removeAlias($this->QBFrom[0]),
648✔
2314
                true,
648✔
2315
                $escape,
648✔
2316
                false
648✔
2317
            ),
648✔
2318
            array_keys($this->QBSet),
648✔
2319
            array_values($this->QBSet)
648✔
2320
        );
648✔
2321

2322
        if (! $this->testMode) {
648✔
2323
            $this->resetWrite();
644✔
2324

2325
            $result = $this->db->query($sql, $this->binds, false);
644✔
2326

2327
            // Clear our binds so we don't eat up memory
2328
            $this->binds = [];
644✔
2329

2330
            return $result;
644✔
2331
        }
2332

2333
        return false;
5✔
2334
    }
2335

2336
    /**
2337
     * @internal This is a temporary solution.
2338
     *
2339
     * @see https://github.com/codeigniter4/CodeIgniter4/pull/5376
2340
     *
2341
     * @TODO Fix a root cause, and this method should be removed.
2342
     */
2343
    protected function removeAlias(string $from): string
2344
    {
2345
        if (str_contains($from, ' ')) {
653✔
2346
            // if the alias is written with the AS keyword, remove it
2347
            $from = preg_replace('/\s+AS\s+/i', ' ', $from);
2✔
2348

2349
            $parts = explode(' ', $from);
2✔
2350
            $from  = $parts[0];
2✔
2351
        }
2352

2353
        return $from;
653✔
2354
    }
2355

2356
    /**
2357
     * This method is used by both insert() and getCompiledInsert() to
2358
     * validate that the there data is actually being set and that table
2359
     * has been chosen to be inserted into.
2360
     *
2361
     * @throws DatabaseException
2362
     */
2363
    protected function validateInsert(): bool
2364
    {
2365
        if (empty($this->QBSet)) {
649✔
2366
            if ($this->db->DBDebug) {
1✔
2367
                throw new DatabaseException('You must use the "set" method to insert an entry.');
1✔
2368
            }
2369

UNCOV
2370
            return false; // @codeCoverageIgnore
×
2371
        }
2372

2373
        return true;
648✔
2374
    }
2375

2376
    /**
2377
     * Generates a platform-specific insert string from the supplied data
2378
     *
2379
     * @param string $table Protected table name
2380
     */
2381
    protected function _insert(string $table, array $keys, array $unescapedKeys): string
2382
    {
2383
        return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')';
642✔
2384
    }
2385

2386
    /**
2387
     * Compiles a replace into string and runs the query
2388
     *
2389
     * @return BaseResult|false|Query|string
2390
     *
2391
     * @throws DatabaseException
2392
     */
2393
    public function replace(?array $set = null)
2394
    {
2395
        if ($set !== null) {
8✔
2396
            $this->set($set);
7✔
2397
        }
2398

2399
        if (empty($this->QBSet)) {
8✔
2400
            if ($this->db->DBDebug) {
1✔
2401
                throw new DatabaseException('You must use the "set" method to update an entry.');
1✔
2402
            }
2403

UNCOV
2404
            return false; // @codeCoverageIgnore
×
2405
        }
2406

2407
        $table = $this->QBFrom[0];
7✔
2408

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

2411
        $this->resetWrite();
7✔
2412

2413
        return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false);
7✔
2414
    }
2415

2416
    /**
2417
     * Generates a platform-specific replace string from the supplied data
2418
     *
2419
     * @param string $table Protected table name
2420
     */
2421
    protected function _replace(string $table, array $keys, array $values): string
2422
    {
2423
        return 'REPLACE INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
7✔
2424
    }
2425

2426
    /**
2427
     * Groups tables in FROM clauses if needed, so there is no confusion
2428
     * about operator precedence.
2429
     *
2430
     * Note: This is only used (and overridden) by MySQL and SQLSRV.
2431
     */
2432
    protected function _fromTables(): string
2433
    {
2434
        return implode(', ', $this->QBFrom);
808✔
2435
    }
2436

2437
    /**
2438
     * Compiles an update query and returns the sql
2439
     *
2440
     * @return bool|string
2441
     */
2442
    public function getCompiledUpdate(bool $reset = true)
2443
    {
2444
        if ($this->validateUpdate() === false) {
13✔
2445
            return false;
×
2446
        }
2447

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

2450
        if ($reset === true) {
13✔
2451
            $this->resetWrite();
13✔
2452
        }
2453

2454
        return $this->compileFinalQuery($sql);
13✔
2455
    }
2456

2457
    /**
2458
     * Compiles an update string and runs the query.
2459
     *
2460
     * @param array|object|null        $set
2461
     * @param array|RawSql|string|null $where
2462
     *
2463
     * @throws DatabaseException
2464
     */
2465
    public function update($set = null, $where = null, ?int $limit = null): bool
2466
    {
2467
        if ($set !== null) {
94✔
2468
            $this->set($set);
47✔
2469
        }
2470

2471
        if ($this->validateUpdate() === false) {
94✔
2472
            return false;
×
2473
        }
2474

2475
        if ($where !== null) {
93✔
2476
            $this->where($where);
8✔
2477
        }
2478

2479
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
93✔
2480
        if ($limitZeroAsAll && $limit === 0) {
93✔
2481
            $limit = null;
×
2482
        }
2483

2484
        if ($limit !== null) {
93✔
2485
            if (! $this->canLimitWhereUpdates) {
3✔
2486
                throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.');
2✔
2487
            }
2488

2489
            $this->limit($limit);
3✔
2490
        }
2491

2492
        $sql = $this->_update($this->QBFrom[0], $this->QBSet);
93✔
2493

2494
        if (! $this->testMode) {
93✔
2495
            $this->resetWrite();
80✔
2496

2497
            $result = $this->db->query($sql, $this->binds, false);
80✔
2498

2499
            if ($result !== false) {
80✔
2500
                // Clear our binds so we don't eat up memory
2501
                $this->binds = [];
77✔
2502

2503
                return true;
77✔
2504
            }
2505

2506
            return false;
3✔
2507
        }
2508

2509
        return true;
13✔
2510
    }
2511

2512
    /**
2513
     * Generates a platform-specific update string from the supplied data
2514
     *
2515
     * @param string $table Protected table name
2516
     */
2517
    protected function _update(string $table, array $values): string
2518
    {
2519
        $valStr = [];
100✔
2520

2521
        foreach ($values as $key => $val) {
100✔
2522
            $valStr[] = $key . ' = ' . $val;
100✔
2523
        }
2524

2525
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
100✔
2526
        if ($limitZeroAsAll) {
100✔
2527
            return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
100✔
2528
                . $this->compileWhereHaving('QBWhere')
100✔
2529
                . $this->compileOrderBy()
100✔
2530
                . ($this->QBLimit ? $this->_limit(' ', true) : '');
100✔
2531
        }
2532

2533
        return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
×
2534
            . $this->compileWhereHaving('QBWhere')
×
2535
            . $this->compileOrderBy()
×
2536
            . ($this->QBLimit !== false ? $this->_limit(' ', true) : '');
×
2537
    }
2538

2539
    /**
2540
     * This method is used by both update() and getCompiledUpdate() to
2541
     * validate that data is actually being set and that a table has been
2542
     * chosen to be updated.
2543
     *
2544
     * @throws DatabaseException
2545
     */
2546
    protected function validateUpdate(): bool
2547
    {
2548
        if (empty($this->QBSet)) {
95✔
2549
            if ($this->db->DBDebug) {
1✔
2550
                throw new DatabaseException('You must use the "set" method to update an entry.');
1✔
2551
            }
2552

UNCOV
2553
            return false; // @codeCoverageIgnore
×
2554
        }
2555

2556
        return true;
94✔
2557
    }
2558

2559
    /**
2560
     * Sets data and calls batchExecute to run queries
2561
     *
2562
     * @param array|object|null        $set         a dataset
2563
     * @param array|RawSql|string|null $constraints
2564
     *
2565
     * @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
2566
     */
2567
    public function updateBatch($set = null, $constraints = null, int $batchSize = 100)
2568
    {
2569
        $this->onConstraint($constraints);
22✔
2570

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

2574
            if ($sql === '') {
1✔
UNCOV
2575
                return false; // @codeCoverageIgnore
×
2576
            }
2577

2578
            if ($this->testMode === false) {
1✔
2579
                $this->db->query($sql, null, false);
1✔
2580
            }
2581

2582
            $this->resetWrite();
1✔
2583

2584
            return $this->testMode ? $sql : $this->db->affectedRows();
1✔
2585
        }
2586

2587
        if ($set !== null && $set !== []) {
21✔
2588
            $this->setData($set, true);
14✔
2589
        }
2590

2591
        return $this->batchExecute('_updateBatch', $batchSize);
21✔
2592
    }
2593

2594
    /**
2595
     * Generates a platform-specific batch update string from the supplied data
2596
     *
2597
     * @used-by batchExecute()
2598
     *
2599
     * @param string                 $table  Protected table name
2600
     * @param list<string>           $keys   QBKeys
2601
     * @param list<list<int|string>> $values QBSet
2602
     */
2603
    protected function _updateBatch(string $table, array $keys, array $values): string
2604
    {
2605
        $sql = $this->QBOptions['sql'] ?? '';
17✔
2606

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

2611
            if ($constraints === []) {
17✔
2612
                if ($this->db->DBDebug) {
2✔
2613
                    throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
2✔
2614
                }
2615

UNCOV
2616
                return ''; // @codeCoverageIgnore
×
2617
            }
2618

2619
            $updateFields = $this->QBOptions['updateFields'] ??
15✔
2620
                $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
15✔
2621
                [];
12✔
2622

2623
            $alias = $this->QBOptions['alias'] ?? '_u';
15✔
2624

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

2627
            $sql .= "SET\n";
15✔
2628

2629
            $sql .= implode(
15✔
2630
                ",\n",
15✔
2631
                array_map(
15✔
2632
                    static fn ($key, $value) => $key . ($value instanceof RawSql ?
15✔
2633
                        ' = ' . $value :
2✔
2634
                        ' = ' . $alias . '.' . $value),
15✔
2635
                    array_keys($updateFields),
15✔
2636
                    $updateFields
15✔
2637
                )
15✔
2638
            ) . "\n";
15✔
2639

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

2642
            $sql .= ') ' . $alias . "\n";
15✔
2643

2644
            $sql .= 'WHERE ' . implode(
15✔
2645
                ' AND ',
15✔
2646
                array_map(
15✔
2647
                    static fn ($key, $value) => (
15✔
2648
                        ($value instanceof RawSql && is_string($key))
15✔
2649
                        ?
15✔
2650
                        $table . '.' . $key . ' = ' . $value
1✔
2651
                        :
15✔
2652
                        (
15✔
2653
                            $value instanceof RawSql
14✔
2654
                            ?
14✔
2655
                            $value
3✔
2656
                            :
14✔
2657
                            $table . '.' . $value . ' = ' . $alias . '.' . $value
15✔
2658
                        )
15✔
2659
                    ),
15✔
2660
                    array_keys($constraints),
15✔
2661
                    $constraints
15✔
2662
                )
15✔
2663
            );
15✔
2664

2665
            $this->QBOptions['sql'] = $sql;
15✔
2666
        }
2667

2668
        if (isset($this->QBOptions['setQueryAsData'])) {
15✔
2669
            $data = $this->QBOptions['setQueryAsData'];
1✔
2670
        } else {
2671
            $data = implode(
14✔
2672
                " UNION ALL\n",
14✔
2673
                array_map(
14✔
2674
                    static fn ($value) => 'SELECT ' . implode(', ', array_map(
14✔
2675
                        static fn ($key, $index) => $index . ' ' . $key,
14✔
2676
                        $keys,
14✔
2677
                        $value
14✔
2678
                    )),
14✔
2679
                    $values
14✔
2680
                )
14✔
2681
            ) . "\n";
14✔
2682
        }
2683

2684
        return str_replace('{:_table_:}', $data, $sql);
15✔
2685
    }
2686

2687
    /**
2688
     * Allows key/value pairs to be set for batch updating
2689
     *
2690
     * @param array|object $key
2691
     *
2692
     * @return $this
2693
     *
2694
     * @throws DatabaseException
2695
     *
2696
     * @deprecated
2697
     */
2698
    public function setUpdateBatch($key, string $index = '', ?bool $escape = null)
2699
    {
2700
        if ($index !== '') {
2✔
2701
            $this->onConstraint($index);
2✔
2702
        }
2703

2704
        $this->setData($key, $escape);
2✔
2705

2706
        return $this;
2✔
2707
    }
2708

2709
    /**
2710
     * Compiles a delete string and runs "DELETE FROM table"
2711
     *
2712
     * @return bool|string TRUE on success, FALSE on failure, string on testMode
2713
     */
2714
    public function emptyTable()
2715
    {
2716
        $table = $this->QBFrom[0];
4✔
2717

2718
        $sql = $this->_delete($table);
4✔
2719

2720
        if ($this->testMode) {
4✔
2721
            return $sql;
1✔
2722
        }
2723

2724
        $this->resetWrite();
3✔
2725

2726
        return $this->db->query($sql, null, false);
3✔
2727
    }
2728

2729
    /**
2730
     * Compiles a truncate string and runs the query
2731
     * If the database does not support the truncate() command
2732
     * This function maps to "DELETE FROM table"
2733
     *
2734
     * @return bool|string TRUE on success, FALSE on failure, string on testMode
2735
     */
2736
    public function truncate()
2737
    {
2738
        $table = $this->QBFrom[0];
583✔
2739

2740
        $sql = $this->_truncate($table);
583✔
2741

2742
        if ($this->testMode) {
583✔
2743
            return $sql;
2✔
2744
        }
2745

2746
        $this->resetWrite();
582✔
2747

2748
        return $this->db->query($sql, null, false);
582✔
2749
    }
2750

2751
    /**
2752
     * Generates a platform-specific truncate string from the supplied data
2753
     *
2754
     * If the database does not support the truncate() command,
2755
     * then this method maps to 'DELETE FROM table'
2756
     *
2757
     * @param string $table Protected table name
2758
     */
2759
    protected function _truncate(string $table): string
2760
    {
2761
        return 'TRUNCATE ' . $table;
571✔
2762
    }
2763

2764
    /**
2765
     * Compiles a delete query string and returns the sql
2766
     */
2767
    public function getCompiledDelete(bool $reset = true): string
2768
    {
2769
        $sql = $this->testMode()->delete('', null, $reset);
3✔
2770
        $this->testMode(false);
3✔
2771

2772
        return $this->compileFinalQuery($sql);
3✔
2773
    }
2774

2775
    /**
2776
     * Compiles a delete string and runs the query
2777
     *
2778
     * @param array|RawSql|string $where
2779
     *
2780
     * @return bool|string Returns a SQL string if in test mode.
2781
     *
2782
     * @throws DatabaseException
2783
     */
2784
    public function delete($where = '', ?int $limit = null, bool $resetData = true)
2785
    {
2786
        $table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);
610✔
2787

2788
        if ($where !== '') {
610✔
2789
            $this->where($where);
4✔
2790
        }
2791

2792
        if (empty($this->QBWhere)) {
610✔
2793
            if ($this->db->DBDebug) {
4✔
2794
                throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.');
4✔
2795
            }
2796

UNCOV
2797
            return false; // @codeCoverageIgnore
×
2798
        }
2799

2800
        $sql = $this->_delete($this->removeAlias($table));
610✔
2801

2802
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
610✔
2803
        if ($limitZeroAsAll && $limit === 0) {
610✔
2804
            $limit = null;
×
2805
        }
2806

2807
        if ($limit !== null) {
610✔
2808
            $this->QBLimit = $limit;
1✔
2809
        }
2810

2811
        if (! empty($this->QBLimit)) {
610✔
2812
            if (! $this->canLimitDeletes) {
2✔
2813
                throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.');
1✔
2814
            }
2815

2816
            $sql = $this->_limit($sql, true);
2✔
2817
        }
2818

2819
        if ($resetData) {
610✔
2820
            $this->resetWrite();
610✔
2821
        }
2822

2823
        return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false);
610✔
2824
    }
2825

2826
    /**
2827
     * Sets data and calls batchExecute to run queries
2828
     *
2829
     * @param array|object|null $set         a dataset
2830
     * @param array|RawSql|null $constraints
2831
     *
2832
     * @return false|int|list<string> Number of rows affected or FALSE on failure, SQL array when testMode
2833
     */
2834
    public function deleteBatch($set = null, $constraints = null, int $batchSize = 100)
2835
    {
2836
        $this->onConstraint($constraints);
3✔
2837

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

2841
            if ($sql === '') {
1✔
UNCOV
2842
                return false; // @codeCoverageIgnore
×
2843
            }
2844

2845
            if ($this->testMode === false) {
1✔
2846
                $this->db->query($sql, null, false);
1✔
2847
            }
2848

2849
            $this->resetWrite();
1✔
2850

2851
            return $this->testMode ? $sql : $this->db->affectedRows();
1✔
2852
        }
2853

2854
        if ($set !== null && $set !== []) {
2✔
2855
            $this->setData($set, true);
×
2856
        }
2857

2858
        return $this->batchExecute('_deleteBatch', $batchSize);
2✔
2859
    }
2860

2861
    /**
2862
     * Generates a platform-specific batch update string from the supplied data
2863
     *
2864
     * @used-by batchExecute()
2865
     *
2866
     * @param string       $table Protected table name
2867
     * @param list<string> $keys  QBKeys
2868
     * @paramst<string|int>> $values QBSet
2869
     */
2870
    protected function _deleteBatch(string $table, array $keys, array $values): string
2871
    {
2872
        $sql = $this->QBOptions['sql'] ?? '';
3✔
2873

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

2878
            if ($constraints === []) {
3✔
2879
                if ($this->db->DBDebug) {
×
UNCOV
2880
                    throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
×
2881
                }
2882

UNCOV
2883
                return ''; // @codeCoverageIgnore
×
2884
            }
2885

2886
            $alias = $this->QBOptions['alias'] ?? '_u';
3✔
2887

2888
            $sql = 'DELETE ' . $table . ' FROM ' . $table . "\n";
3✔
2889

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

2892
            $sql .= ') ' . $alias . "\n";
3✔
2893

2894
            $sql .= 'ON ' . implode(
3✔
2895
                ' AND ',
3✔
2896
                array_map(
3✔
2897
                    static fn ($key, $value) => (
3✔
2898
                        $value instanceof RawSql ?
3✔
2899
                        $value :
×
2900
                        (
3✔
2901
                            is_string($key) ?
3✔
2902
                            $table . '.' . $key . ' = ' . $alias . '.' . $value :
2✔
2903
                            $table . '.' . $value . ' = ' . $alias . '.' . $value
3✔
2904
                        )
3✔
2905
                    ),
3✔
2906
                    array_keys($constraints),
3✔
2907
                    $constraints
3✔
2908
                )
3✔
2909
            );
3✔
2910

2911
            // convert binds in where
2912
            foreach ($this->QBWhere as $key => $where) {
3✔
2913
                foreach ($this->binds as $field => $bind) {
2✔
2914
                    $this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']);
×
2915
                }
2916
            }
2917

2918
            $sql .= ' ' . $this->compileWhereHaving('QBWhere');
3✔
2919

2920
            $this->QBOptions['sql'] = trim($sql);
3✔
2921
        }
2922

2923
        if (isset($this->QBOptions['setQueryAsData'])) {
3✔
2924
            $data = $this->QBOptions['setQueryAsData'];
1✔
2925
        } else {
2926
            $data = implode(
2✔
2927
                " UNION ALL\n",
2✔
2928
                array_map(
2✔
2929
                    static fn ($value) => 'SELECT ' . implode(', ', array_map(
2✔
2930
                        static fn ($key, $index) => $index . ' ' . $key,
2✔
2931
                        $keys,
2✔
2932
                        $value
2✔
2933
                    )),
2✔
2934
                    $values
2✔
2935
                )
2✔
2936
            ) . "\n";
2✔
2937
        }
2938

2939
        return str_replace('{:_table_:}', $data, $sql);
3✔
2940
    }
2941

2942
    /**
2943
     * Increments a numeric column by the specified value.
2944
     *
2945
     * @return bool
2946
     */
2947
    public function increment(string $column, int $value = 1)
2948
    {
2949
        $column = $this->db->protectIdentifiers($column);
3✔
2950

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

2953
        if (! $this->testMode) {
3✔
2954
            $this->resetWrite();
3✔
2955

2956
            return $this->db->query($sql, $this->binds, false);
3✔
2957
        }
2958

2959
        return true;
×
2960
    }
2961

2962
    /**
2963
     * Decrements a numeric column by the specified value.
2964
     *
2965
     * @return bool
2966
     */
2967
    public function decrement(string $column, int $value = 1)
2968
    {
2969
        $column = $this->db->protectIdentifiers($column);
3✔
2970

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

2973
        if (! $this->testMode) {
3✔
2974
            $this->resetWrite();
3✔
2975

2976
            return $this->db->query($sql, $this->binds, false);
3✔
2977
        }
2978

2979
        return true;
×
2980
    }
2981

2982
    /**
2983
     * Generates a platform-specific delete string from the supplied data
2984
     *
2985
     * @param string $table Protected table name
2986
     */
2987
    protected function _delete(string $table): string
2988
    {
2989
        return 'DELETE ' . $this->compileIgnore('delete') . 'FROM ' . $table . $this->compileWhereHaving('QBWhere');
613✔
2990
    }
2991

2992
    /**
2993
     * Used to track SQL statements written with aliased tables.
2994
     *
2995
     * @param array|string $table The table to inspect
2996
     *
2997
     * @return string|void
2998
     */
2999
    protected function trackAliases($table)
3000
    {
3001
        if (is_array($table)) {
888✔
3002
            foreach ($table as $t) {
×
3003
                $this->trackAliases($t);
×
3004
            }
3005

3006
            return;
×
3007
        }
3008

3009
        // Does the string contain a comma?  If so, we need to separate
3010
        // the string into discreet statements
3011
        if (str_contains($table, ',')) {
888✔
3012
            return $this->trackAliases(explode(',', $table));
×
3013
        }
3014

3015
        // if a table alias is used we can recognize it by a space
3016
        if (str_contains($table, ' ')) {
888✔
3017
            // if the alias is written with the AS keyword, remove it
3018
            $table = preg_replace('/\s+AS\s+/i', ' ', $table);
15✔
3019

3020
            // Grab the alias
3021
            $table = trim(strrchr($table, ' '));
15✔
3022

3023
            // Store the alias, if it doesn't already exist
3024
            $this->db->addTableAlias($table);
15✔
3025
        }
3026
    }
3027

3028
    /**
3029
     * Compile the SELECT statement
3030
     *
3031
     * Generates a query string based on which functions were used.
3032
     * Should not be called directly.
3033
     *
3034
     * @param mixed $selectOverride
3035
     */
3036
    protected function compileSelect($selectOverride = false): string
3037
    {
3038
        if ($selectOverride !== false) {
826✔
3039
            $sql = $selectOverride;
154✔
3040
        } else {
3041
            $sql = (! $this->QBDistinct) ? 'SELECT ' : 'SELECT DISTINCT ';
823✔
3042

3043
            if (empty($this->QBSelect)) {
823✔
3044
                $sql .= '*';
749✔
3045
            } elseif ($this->QBSelect[0] instanceof RawSql) {
706✔
3046
                $sql .= (string) $this->QBSelect[0];
1✔
3047
            } else {
3048
                // Cycle through the "select" portion of the query and prep each column name.
3049
                // The reason we protect identifiers here rather than in the select() function
3050
                // is because until the user calls the from() function we don't know if there are aliases
3051
                foreach ($this->QBSelect as $key => $val) {
705✔
3052
                    $noEscape             = $this->QBNoEscape[$key] ?? null;
705✔
3053
                    $this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $noEscape);
705✔
3054
                }
3055

3056
                $sql .= implode(', ', $this->QBSelect);
705✔
3057
            }
3058
        }
3059

3060
        if (! empty($this->QBFrom)) {
826✔
3061
            $sql .= "\nFROM " . $this->_fromTables();
826✔
3062
        }
3063

3064
        if (! empty($this->QBJoin)) {
826✔
3065
            $sql .= "\n" . implode("\n", $this->QBJoin);
14✔
3066
        }
3067

3068
        $sql .= $this->compileWhereHaving('QBWhere')
826✔
3069
            . $this->compileGroupBy()
826✔
3070
            . $this->compileWhereHaving('QBHaving')
826✔
3071
            . $this->compileOrderBy();
826✔
3072

3073
        $limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
826✔
3074
        if ($limitZeroAsAll) {
826✔
3075
            if ($this->QBLimit) {
825✔
3076
                $sql = $this->_limit($sql . "\n");
82✔
3077
            }
3078
        } elseif ($this->QBLimit !== false || $this->QBOffset) {
2✔
3079
            $sql = $this->_limit($sql . "\n");
2✔
3080
        }
3081

3082
        return $this->unionInjection($sql);
826✔
3083
    }
3084

3085
    /**
3086
     * Checks if the ignore option is supported by
3087
     * the Database Driver for the specific statement.
3088
     *
3089
     * @return string
3090
     */
3091
    protected function compileIgnore(string $statement)
3092
    {
3093
        if ($this->QBIgnore && isset($this->supportedIgnoreStatements[$statement])) {
672✔
3094
            return trim($this->supportedIgnoreStatements[$statement]) . ' ';
1✔
3095
        }
3096

3097
        return '';
671✔
3098
    }
3099

3100
    /**
3101
     * Escapes identifiers in WHERE and HAVING statements at execution time.
3102
     *
3103
     * Required so that aliases are tracked properly, regardless of whether
3104
     * where(), orWhere(), having(), orHaving are called prior to from(),
3105
     * join() and prefixTable is added only if needed.
3106
     *
3107
     * @param string $qbKey 'QBWhere' or 'QBHaving'
3108
     *
3109
     * @return string SQL statement
3110
     */
3111
    protected function compileWhereHaving(string $qbKey): string
3112
    {
3113
        if (! empty($this->{$qbKey})) {
848✔
3114
            foreach ($this->{$qbKey} as &$qbkey) {
764✔
3115
                // Is this condition already compiled?
3116
                if (is_string($qbkey)) {
764✔
3117
                    continue;
23✔
3118
                }
3119

3120
                if ($qbkey instanceof RawSql) {
764✔
3121
                    continue;
2✔
3122
                }
3123

3124
                if ($qbkey['condition'] instanceof RawSql) {
764✔
3125
                    $qbkey = $qbkey['condition'];
4✔
3126

3127
                    continue;
4✔
3128
                }
3129

3130
                if ($qbkey['escape'] === false) {
762✔
3131
                    $qbkey = $qbkey['condition'];
103✔
3132

3133
                    continue;
103✔
3134
                }
3135

3136
                // Split multiple conditions
3137
                $conditions = preg_split(
752✔
3138
                    '/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i',
752✔
3139
                    $qbkey['condition'],
752✔
3140
                    -1,
752✔
3141
                    PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
752✔
3142
                );
752✔
3143

3144
                foreach ($conditions as &$condition) {
752✔
3145
                    if (($op = $this->getOperator($condition)) === false
752✔
3146
                        || ! preg_match('/^(\(?)(.*)(' . preg_quote($op, '/') . ')\s*(.*(?<!\)))?(\)?)$/i', $condition, $matches)
752✔
3147
                    ) {
3148
                        continue;
655✔
3149
                    }
3150
                    // $matches = array(
3151
                    //        0 => '(test <= foo)',        /* the whole thing */
3152
                    //        1 => '(',                /* optional */
3153
                    //        2 => 'test',                /* the field name */
3154
                    //        3 => ' <= ',                /* $op */
3155
                    //        4 => 'foo',                /* optional, if $op is e.g. 'IS NULL' */
3156
                    //        5 => ')'                /* optional */
3157
                    // );
3158

3159
                    if (! empty($matches[4])) {
752✔
3160
                        $protectIdentifiers = false;
719✔
3161
                        if (str_contains($matches[4], '.')) {
719✔
3162
                            $protectIdentifiers = true;
79✔
3163
                        }
3164

3165
                        if (! str_contains($matches[4], ':')) {
719✔
3166
                            $matches[4] = $this->db->protectIdentifiers(trim($matches[4]), false, $protectIdentifiers);
14✔
3167
                        }
3168

3169
                        $matches[4] = ' ' . $matches[4];
719✔
3170
                    }
3171

3172
                    $condition = $matches[1] . $this->db->protectIdentifiers(trim($matches[2]))
752✔
3173
                        . ' ' . trim($matches[3]) . $matches[4] . $matches[5];
752✔
3174
                }
3175

3176
                $qbkey = implode('', $conditions);
752✔
3177
            }
3178

3179
            return ($qbKey === 'QBHaving' ? "\nHAVING " : "\nWHERE ")
764✔
3180
                . implode("\n", $this->{$qbKey});
764✔
3181
        }
3182

3183
        return '';
830✔
3184
    }
3185

3186
    /**
3187
     * Escapes identifiers in GROUP BY statements at execution time.
3188
     *
3189
     * Required so that aliases are tracked properly, regardless of whether
3190
     * groupBy() is called prior to from(), join() and prefixTable is added
3191
     * only if needed.
3192
     */
3193
    protected function compileGroupBy(): string
3194
    {
3195
        if (! empty($this->QBGroupBy)) {
831✔
3196
            foreach ($this->QBGroupBy as &$groupBy) {
56✔
3197
                // Is it already compiled?
3198
                if (is_string($groupBy)) {
56✔
3199
                    continue;
2✔
3200
                }
3201

3202
                $groupBy = ($groupBy['escape'] === false || $this->isLiteral($groupBy['field']))
56✔
3203
                    ? $groupBy['field']
×
3204
                    : $this->db->protectIdentifiers($groupBy['field']);
56✔
3205
            }
3206

3207
            return "\nGROUP BY " . implode(', ', $this->QBGroupBy);
56✔
3208
        }
3209

3210
        return '';
805✔
3211
    }
3212

3213
    /**
3214
     * Escapes identifiers in ORDER BY statements at execution time.
3215
     *
3216
     * Required so that aliases are tracked properly, regardless of whether
3217
     * orderBy() is called prior to from(), join() and prefixTable is added
3218
     * only if needed.
3219
     */
3220
    protected function compileOrderBy(): string
3221
    {
3222
        if (is_array($this->QBOrderBy) && $this->QBOrderBy !== []) {
843✔
3223
            foreach ($this->QBOrderBy as &$orderBy) {
647✔
3224
                if ($orderBy['escape'] !== false && ! $this->isLiteral($orderBy['field'])) {
647✔
3225
                    $orderBy['field'] = $this->db->protectIdentifiers($orderBy['field']);
645✔
3226
                }
3227

3228
                $orderBy = $orderBy['field'] . $orderBy['direction'];
647✔
3229
            }
3230

3231
            return $this->QBOrderBy = "\nORDER BY " . implode(', ', $this->QBOrderBy);
647✔
3232
        }
3233

3234
        if (is_string($this->QBOrderBy)) {
818✔
3235
            return $this->QBOrderBy;
×
3236
        }
3237

3238
        return '';
818✔
3239
    }
3240

3241
    protected function unionInjection(string $sql): string
3242
    {
3243
        if ($this->QBUnion === []) {
831✔
3244
            return $sql;
831✔
3245
        }
3246

3247
        return 'SELECT * FROM (' . $sql . ') '
6✔
3248
            . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers('uwrp0') : 'uwrp0')
6✔
3249
            . implode("\n", $this->QBUnion);
6✔
3250
    }
3251

3252
    /**
3253
     * Takes an object as input and converts the class variables to array key/vals
3254
     *
3255
     * @param array|object $object
3256
     *
3257
     * @return array
3258
     */
3259
    protected function objectToArray($object)
3260
    {
3261
        if (! is_object($object)) {
669✔
3262
            return $object;
666✔
3263
        }
3264

3265
        if ($object instanceof RawSql) {
10✔
3266
            throw new InvalidArgumentException('RawSql "' . $object . '" cannot be used here.');
1✔
3267
        }
3268

3269
        $array = [];
9✔
3270

3271
        foreach (get_object_vars($object) as $key => $val) {
9✔
3272
            if ((! is_object($val) || $val instanceof RawSql) && ! is_array($val)) {
9✔
3273
                $array[$key] = $val;
9✔
3274
            }
3275
        }
3276

3277
        return $array;
9✔
3278
    }
3279

3280
    /**
3281
     * Takes an object as input and converts the class variables to array key/vals
3282
     *
3283
     * @param array|object $object
3284
     *
3285
     * @return array
3286
     */
3287
    protected function batchObjectToArray($object)
3288
    {
3289
        if (! is_object($object)) {
64✔
3290
            return $object;
64✔
3291
        }
3292

3293
        $array  = [];
×
3294
        $out    = get_object_vars($object);
×
3295
        $fields = array_keys($out);
×
3296

3297
        foreach ($fields as $val) {
×
3298
            $i = 0;
×
3299

3300
            foreach ($out[$val] as $data) {
×
3301
                $array[$i++][$val] = $data;
×
3302
            }
3303
        }
3304

3305
        return $array;
×
3306
    }
3307

3308
    /**
3309
     * Determines if a string represents a literal value or a field name
3310
     */
3311
    protected function isLiteral(string $str): bool
3312
    {
3313
        $str = trim($str);
675✔
3314

3315
        if ($str === ''
675✔
3316
            || ctype_digit($str)
675✔
3317
            || (string) (float) $str === $str
675✔
3318
            || in_array(strtoupper($str), ['TRUE', 'FALSE'], true)
675✔
3319
        ) {
3320
            return true;
×
3321
        }
3322

3323
        if ($this->isLiteralStr === []) {
675✔
3324
            $this->isLiteralStr = $this->db->escapeChar !== '"' ? ['"', "'"] : ["'"];
675✔
3325
        }
3326

3327
        return in_array($str[0], $this->isLiteralStr, true);
675✔
3328
    }
3329

3330
    /**
3331
     * Publicly-visible method to reset the QB values.
3332
     *
3333
     * @return $this
3334
     */
3335
    public function resetQuery()
3336
    {
3337
        $this->resetSelect();
1✔
3338
        $this->resetWrite();
1✔
3339

3340
        return $this;
1✔
3341
    }
3342

3343
    /**
3344
     * Resets the query builder values.  Called by the get() function
3345
     *
3346
     * @param array $qbResetItems An array of fields to reset
3347
     */
3348
    protected function resetRun(array $qbResetItems)
3349
    {
3350
        foreach ($qbResetItems as $item => $defaultValue) {
853✔
3351
            $this->{$item} = $defaultValue;
853✔
3352
        }
3353
    }
3354

3355
    /**
3356
     * Resets the query builder values.  Called by the get() function
3357
     */
3358
    protected function resetSelect()
3359
    {
3360
        $this->resetRun([
826✔
3361
            'QBSelect'   => [],
826✔
3362
            'QBJoin'     => [],
826✔
3363
            'QBWhere'    => [],
826✔
3364
            'QBGroupBy'  => [],
826✔
3365
            'QBHaving'   => [],
826✔
3366
            'QBOrderBy'  => [],
826✔
3367
            'QBNoEscape' => [],
826✔
3368
            'QBDistinct' => false,
826✔
3369
            'QBLimit'    => false,
826✔
3370
            'QBOffset'   => false,
826✔
3371
            'QBUnion'    => [],
826✔
3372
        ]);
826✔
3373

3374
        if (! empty($this->db)) {
826✔
3375
            $this->db->setAliasedTables([]);
826✔
3376
        }
3377

3378
        // Reset QBFrom part
3379
        if (! empty($this->QBFrom)) {
826✔
3380
            $this->from(array_shift($this->QBFrom), true);
826✔
3381
        }
3382
    }
3383

3384
    /**
3385
     * Resets the query builder "write" values.
3386
     *
3387
     * Called by the insert() update() insertBatch() updateBatch() and delete() functions
3388
     */
3389
    protected function resetWrite()
3390
    {
3391
        $this->resetRun([
673✔
3392
            'QBSet'     => [],
673✔
3393
            'QBJoin'    => [],
673✔
3394
            'QBWhere'   => [],
673✔
3395
            'QBOrderBy' => [],
673✔
3396
            'QBKeys'    => [],
673✔
3397
            'QBLimit'   => false,
673✔
3398
            'QBIgnore'  => false,
673✔
3399
            'QBOptions' => [],
673✔
3400
        ]);
673✔
3401
    }
3402

3403
    /**
3404
     * Tests whether the string has an SQL operator
3405
     */
3406
    protected function hasOperator(string $str): bool
3407
    {
3408
        return preg_match(
117✔
3409
            '/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i',
117✔
3410
            trim($str)
117✔
3411
        ) === 1;
117✔
3412
    }
3413

3414
    /**
3415
     * Returns the SQL string operator
3416
     *
3417
     * @return array|false|string
3418
     */
3419
    protected function getOperator(string $str, bool $list = false)
3420
    {
3421
        if ($this->pregOperators === []) {
760✔
3422
            $_les = $this->db->likeEscapeStr !== ''
760✔
3423
                ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/')
760✔
3424
                : '';
×
3425
            $this->pregOperators = [
760✔
3426
                '\s*(?:<|>|!)?=\s*', // =, <=, >=, !=
760✔
3427
                '\s*<>?\s*',         // <, <>
760✔
3428
                '\s*>\s*',           // >
760✔
3429
                '\s+IS NULL',             // IS NULL
760✔
3430
                '\s+IS NOT NULL',         // IS NOT NULL
760✔
3431
                '\s+EXISTS\s*\(.*\)',     // EXISTS (sql)
760✔
3432
                '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql)
760✔
3433
                '\s+BETWEEN\s+',          // BETWEEN value AND value
760✔
3434
                '\s+IN\s*\(.*\)',         // IN (list)
760✔
3435
                '\s+NOT IN\s*\(.*\)',     // NOT IN (list)
760✔
3436
                '\s+LIKE\s+\S.*(' . $_les . ')?',     // LIKE 'expr'[ ESCAPE '%s']
760✔
3437
                '\s+NOT LIKE\s+\S.*(' . $_les . ')?', // NOT LIKE 'expr'[ ESCAPE '%s']
760✔
3438
            ];
760✔
3439
        }
3440

3441
        return preg_match_all(
760✔
3442
            '/' . implode('|', $this->pregOperators) . '/i',
760✔
3443
            $str,
760✔
3444
            $match
760✔
3445
        ) ? ($list ? $match[0] : $match[0][0]) : false;
760✔
3446
    }
3447

3448
    /**
3449
     * Returns the SQL string operator from where key
3450
     *
3451
     * @return false|list<string>
3452
     */
3453
    private function getOperatorFromWhereKey(string $whereKey)
3454
    {
3455
        $whereKey = trim($whereKey);
718✔
3456

3457
        $pregOperators = [
718✔
3458
            '\s*(?:<|>|!)?=',         // =, <=, >=, !=
718✔
3459
            '\s*<>?',                 // <, <>
718✔
3460
            '\s*>',                   // >
718✔
3461
            '\s+IS NULL',             // IS NULL
718✔
3462
            '\s+IS NOT NULL',         // IS NOT NULL
718✔
3463
            '\s+EXISTS\s*\(.*\)',     // EXISTS (sql)
718✔
3464
            '\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS (sql)
718✔
3465
            '\s+BETWEEN\s+',          // BETWEEN value AND value
718✔
3466
            '\s+IN\s*\(.*\)',         // IN (list)
718✔
3467
            '\s+NOT IN\s*\(.*\)',     // NOT IN (list)
718✔
3468
            '\s+LIKE',                // LIKE
718✔
3469
            '\s+NOT LIKE',            // NOT LIKE
718✔
3470
        ];
718✔
3471

3472
        return preg_match_all(
718✔
3473
            '/' . implode('|', $pregOperators) . '/i',
718✔
3474
            $whereKey,
718✔
3475
            $match
718✔
3476
        ) ? $match[0] : false;
718✔
3477
    }
3478

3479
    /**
3480
     * Stores a bind value after ensuring that it's unique.
3481
     * While it might be nicer to have named keys for our binds array
3482
     * with PHP 7+ we get a huge memory/performance gain with indexed
3483
     * arrays instead, so lets take advantage of that here.
3484
     *
3485
     * @param mixed $value
3486
     */
3487
    protected function setBind(string $key, $value = null, bool $escape = true): string
3488
    {
3489
        if (! array_key_exists($key, $this->binds)) {
763✔
3490
            $this->binds[$key] = [
763✔
3491
                $value,
763✔
3492
                $escape,
763✔
3493
            ];
763✔
3494

3495
            return $key;
763✔
3496
        }
3497

3498
        if (! array_key_exists($key, $this->bindsKeyCount)) {
39✔
3499
            $this->bindsKeyCount[$key] = 1;
39✔
3500
        }
3501

3502
        $count = $this->bindsKeyCount[$key]++;
39✔
3503

3504
        $this->binds[$key . '.' . $count] = [
39✔
3505
            $value,
39✔
3506
            $escape,
39✔
3507
        ];
39✔
3508

3509
        return $key . '.' . $count;
39✔
3510
    }
3511

3512
    /**
3513
     * Returns a clone of a Base Builder with reset query builder values.
3514
     *
3515
     * @return $this
3516
     *
3517
     * @deprecated
3518
     */
3519
    protected function cleanClone()
3520
    {
UNCOV
3521
        return (clone $this)->from([], true)->resetQuery();
×
3522
    }
3523

3524
    /**
3525
     * @param mixed $value
3526
     */
3527
    protected function isSubquery($value): bool
3528
    {
3529
        return $value instanceof BaseBuilder || $value instanceof Closure;
726✔
3530
    }
3531

3532
    /**
3533
     * @param BaseBuilder|Closure $builder
3534
     * @param bool                $wrapped Wrap the subquery in brackets
3535
     * @param string              $alias   Subquery alias
3536
     */
3537
    protected function buildSubquery($builder, bool $wrapped = false, string $alias = ''): string
3538
    {
3539
        if ($builder instanceof Closure) {
21✔
3540
            $builder($builder = $this->db->newQuery());
11✔
3541
        }
3542

3543
        if ($builder === $this) {
21✔
3544
            throw new DatabaseException('The subquery cannot be the same object as the main query object.');
1✔
3545
        }
3546

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

3549
        if ($wrapped) {
20✔
3550
            $subquery = '(' . $subquery . ')';
20✔
3551
            $alias    = trim($alias);
20✔
3552

3553
            if ($alias !== '') {
20✔
3554
                $subquery .= ' ' . ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers($alias) : $alias);
11✔
3555
            }
3556
        }
3557

3558
        return $subquery;
20✔
3559
    }
3560
}
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

© 2026 Coveralls, Inc