Skip to content

Commit e7cb689

Browse files
authored
Merge pull request #9009 from kenjis/fix-qb-select-rawsql
fix: [QueryBuilder] select() with RawSql may cause TypeError
2 parents bab8828 + c022dd3 commit e7cb689

File tree

3 files changed

+79
-23
lines changed

3 files changed

+79
-23
lines changed

phpstan-baseline.php

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,12 +1819,6 @@
18191819
'count' => 1,
18201820
'path' => __DIR__ . '/system/Database/BaseBuilder.php',
18211821
];
1822-
$ignoreErrors[] = [
1823-
// identifier: missingType.iterableValue
1824-
'message' => '#^Method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:select\\(\\) has parameter \\$select with no value type specified in iterable type array\\.$#',
1825-
'count' => 1,
1826-
'path' => __DIR__ . '/system/Database/BaseBuilder.php',
1827-
];
18281822
$ignoreErrors[] = [
18291823
// identifier: missingType.iterableValue
18301824
'message' => '#^Method CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:set\\(\\) has parameter \\$key with no value type specified in iterable type array\\.$#',
@@ -1993,12 +1987,6 @@
19931987
'count' => 1,
19941988
'path' => __DIR__ . '/system/Database/BaseBuilder.php',
19951989
];
1996-
$ignoreErrors[] = [
1997-
// identifier: missingType.iterableValue
1998-
'message' => '#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$QBNoEscape type has no value type specified in iterable type array\\.$#',
1999-
'count' => 1,
2000-
'path' => __DIR__ . '/system/Database/BaseBuilder.php',
2001-
];
20021990
$ignoreErrors[] = [
20031991
// identifier: missingType.iterableValue
20041992
'message' => '#^Property CodeIgniter\\\\Database\\\\BaseBuilder\\:\\:\\$QBOptions type has no value type specified in iterable type array\\.$#',

system/Database/BaseBuilder.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ class BaseBuilder
124124
protected array $QBUnion = [];
125125

126126
/**
127-
* QB NO ESCAPE data
127+
* Whether to protect identifiers in SELECT
128128
*
129-
* @var array
129+
* @var list<bool|null> true=protect, false=not protect
130130
*/
131131
public $QBNoEscape = [];
132132

@@ -390,7 +390,8 @@ public function ignore(bool $ignore = true)
390390
/**
391391
* Generates the SELECT portion of the query
392392
*
393-
* @param array|RawSql|string $select
393+
* @param list<RawSql|string>|RawSql|string $select
394+
* @param bool|null $escape Whether to protect identifiers
394395
*
395396
* @return $this
396397
*/
@@ -402,16 +403,21 @@ public function select($select = '*', ?bool $escape = null)
402403
}
403404

404405
if ($select instanceof RawSql) {
405-
$this->QBSelect[] = $select;
406-
407-
return $this;
406+
$select = [$select];
408407
}
409408

410409
if (is_string($select)) {
411-
$select = $escape === false ? [$select] : explode(',', $select);
410+
$select = ($escape === false) ? [$select] : explode(',', $select);
412411
}
413412

414413
foreach ($select as $val) {
414+
if ($val instanceof RawSql) {
415+
$this->QBSelect[] = $val;
416+
$this->QBNoEscape[] = false;
417+
418+
continue;
419+
}
420+
415421
$val = trim($val);
416422

417423
if ($val !== '') {
@@ -3054,15 +3060,17 @@ protected function compileSelect($selectOverride = false): string
30543060

30553061
if (empty($this->QBSelect)) {
30563062
$sql .= '*';
3057-
} elseif ($this->QBSelect[0] instanceof RawSql) {
3058-
$sql .= (string) $this->QBSelect[0];
30593063
} else {
30603064
// Cycle through the "select" portion of the query and prep each column name.
30613065
// The reason we protect identifiers here rather than in the select() function
30623066
// is because until the user calls the from() function we don't know if there are aliases
30633067
foreach ($this->QBSelect as $key => $val) {
3064-
$noEscape = $this->QBNoEscape[$key] ?? null;
3065-
$this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $noEscape);
3068+
if ($val instanceof RawSql) {
3069+
$this->QBSelect[$key] = (string) $val;
3070+
} else {
3071+
$protect = $this->QBNoEscape[$key] ?? null;
3072+
$this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $protect);
3073+
}
30663074
}
30673075

30683076
$sql .= implode(', ', $this->QBSelect);

tests/system/Database/Builder/SelectTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use CodeIgniter\Database\SQLSRV\Builder as SQLSRVBuilder;
2020
use CodeIgniter\Test\CIUnitTestCase;
2121
use CodeIgniter\Test\Mock\MockConnection;
22+
use PHPUnit\Framework\Attributes\DataProvider;
2223
use PHPUnit\Framework\Attributes\Group;
2324

2425
/**
@@ -67,6 +68,65 @@ public function testSelectAcceptsArray(): void
6768
$this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect()));
6869
}
6970

71+
/**
72+
* @param list<RawSql|string> $select
73+
*/
74+
#[DataProvider('provideSelectAcceptsArrayWithRawSql')]
75+
public function testSelectAcceptsArrayWithRawSql(array $select, string $expected): void
76+
{
77+
$builder = new BaseBuilder('employees', $this->db);
78+
79+
$builder->select($select);
80+
81+
$this->assertSame($expected, str_replace("\n", ' ', $builder->getCompiledSelect()));
82+
}
83+
84+
/**
85+
* @return list<list<RawSql|string>|string>
86+
*/
87+
public static function provideSelectAcceptsArrayWithRawSql(): iterable
88+
{
89+
yield from [
90+
[
91+
[
92+
new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"),
93+
'employee_id',
94+
],
95+
<<<'SQL'
96+
SELECT IF(salary > 5000, 'High', 'Low') AS salary_level, "employee_id" FROM "employees"
97+
SQL,
98+
],
99+
[
100+
[
101+
'employee_id',
102+
new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"),
103+
],
104+
<<<'SQL'
105+
SELECT "employee_id", IF(salary > 5000, 'High', 'Low') AS salary_level FROM "employees"
106+
SQL,
107+
],
108+
[
109+
[
110+
new RawSql("CONCAT(first_name, ' ', last_name) AS full_name"),
111+
new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"),
112+
],
113+
<<<'SQL'
114+
SELECT CONCAT(first_name, ' ', last_name) AS full_name, IF(salary > 5000, 'High', 'Low') AS salary_level FROM "employees"
115+
SQL,
116+
],
117+
[
118+
[
119+
new RawSql("CONCAT(first_name, ' ', last_name) AS full_name"),
120+
'employee_id',
121+
new RawSql("IF(salary > 5000, 'High', 'Low') AS salary_level"),
122+
],
123+
<<<'SQL'
124+
SELECT CONCAT(first_name, ' ', last_name) AS full_name, "employee_id", IF(salary > 5000, 'High', 'Low') AS salary_level FROM "employees"
125+
SQL,
126+
],
127+
];
128+
}
129+
70130
public function testSelectAcceptsMultipleColumns(): void
71131
{
72132
$builder = new BaseBuilder('users', $this->db);

0 commit comments

Comments
 (0)