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

codeigniter4 / CodeIgniter4 / 25902734269

15 May 2026 05:51AM UTC coverage: 88.459% (+0.2%) from 88.299%
25902734269

Pull #10159

github

web-flow
Merge f0573f3e0 into 170b89a6e
Pull Request #10159: feat: Add support for callable TTLs in cache handlers

6 of 10 new or added lines in 3 files covered. (60.0%)

446 existing lines in 24 files now uncovered.

24114 of 27260 relevant lines covered (88.46%)

219.07 hits per line

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

75.0
/system/Database/SQLite3/Builder.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\SQLite3;
15

16
use CodeIgniter\Database\BaseBuilder;
17
use CodeIgniter\Database\Exceptions\DatabaseException;
18
use CodeIgniter\Database\RawSql;
19
use CodeIgniter\Exceptions\InvalidArgumentException;
20

21
/**
22
 * Builder for SQLite3
23
 */
24
class Builder extends BaseBuilder
25
{
26
    /**
27
     * Default installs of SQLite typically do not
28
     * support limiting delete clauses.
29
     *
30
     * @var bool
31
     */
32
    protected $canLimitDeletes = false;
33

34
    /**
35
     * Default installs of SQLite do no support
36
     * limiting update queries in combo with WHERE.
37
     *
38
     * @var bool
39
     */
40
    protected $canLimitWhereUpdates = false;
41

42
    /**
43
     * ORDER BY random keyword
44
     *
45
     * @var array
46
     */
47
    protected $randomKeyword = [
48
        'RANDOM()',
49
    ];
50

51
    /**
52
     * @var array<string, string>
53
     */
54
    protected $supportedIgnoreStatements = [
55
        'insert' => 'OR IGNORE',
56
    ];
57

58
    /**
59
     * Compile the SELECT lock clause.
60
     */
61
    protected function compileLockForUpdate(): string
62
    {
63
        if ($this->QBLockForUpdate) {
825✔
64
            throw new DatabaseException('SQLite3 does not support lockForUpdate().');
1✔
65
        }
66

67
        return '';
824✔
68
    }
69

70
    /**
71
     * Replace statement
72
     *
73
     * Generates a platform-specific replace string from the supplied data
74
     */
75
    protected function _replace(string $table, array $keys, array $values): string
76
    {
77
        return 'INSERT OR ' . parent::_replace($table, $keys, $values);
6✔
78
    }
79

80
    /**
81
     * Generates a platform-specific truncate string from the supplied data
82
     *
83
     * If the database does not support the TRUNCATE statement,
84
     * then this method maps to 'DELETE FROM table'
85
     */
86
    protected function _truncate(string $table): string
87
    {
88
        return 'DELETE FROM ' . $table;
721✔
89
    }
90

91
    /**
92
     * Generates a platform-specific batch update string from the supplied data
93
     */
94
    protected function _updateBatch(string $table, array $keys, array $values): string
95
    {
96
        if (version_compare($this->db->getVersion(), '3.33.0') >= 0) {
16✔
97
            return parent::_updateBatch($table, $keys, $values);
16✔
98
        }
99

UNCOV
100
        $constraints = $this->QBOptions['constraints'] ?? [];
×
101

102
        if ($constraints === []) {
×
UNCOV
103
            if ($this->db->DBDebug) {
×
104
                throw new DatabaseException('You must specify a constraint to match on for batch updates.');
×
105
            }
106

107
            return ''; // @codeCoverageIgnore
108
        }
109

110
        if (count($constraints) > 1 || isset($this->QBOptions['setQueryAsData']) || (current($constraints) instanceof RawSql)) {
×
UNCOV
111
            throw new DatabaseException('You are trying to use a feature which requires SQLite version 3.33 or higher.');
×
112
        }
113

114
        $index = current($constraints);
×
115

UNCOV
116
        $ids   = [];
×
UNCOV
117
        $final = [];
×
118

119
        foreach ($values as $val) {
×
UNCOV
120
            $val = array_combine($keys, $val);
×
121

122
            $ids[] = $val[$index];
×
123

124
            foreach (array_keys($val) as $field) {
×
UNCOV
125
                if ($field !== $index) {
×
UNCOV
126
                    $final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
×
127
                }
128
            }
129
        }
130

UNCOV
131
        $cases = '';
×
132

UNCOV
133
        foreach ($final as $k => $v) {
×
UNCOV
134
            $cases .= $k . " = CASE \n"
×
UNCOV
135
                . implode("\n", $v) . "\n"
×
UNCOV
136
                . 'ELSE ' . $k . ' END, ';
×
137
        }
138

UNCOV
139
        $this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);
×
140

UNCOV
141
        return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
×
142
    }
143

144
    /**
145
     * Generates a platform-specific upsertBatch string from the supplied data
146
     *
147
     * @throws DatabaseException
148
     */
149
    protected function _upsertBatch(string $table, array $keys, array $values): string
150
    {
151
        $sql = $this->QBOptions['sql'] ?? '';
19✔
152

153
        // if this is the first iteration of batch then we need to build skeleton sql
154
        if ($sql === '') {
19✔
155
            $constraints = $this->QBOptions['constraints'] ?? [];
19✔
156

157
            if (empty($constraints)) {
19✔
158
                $fieldNames = array_map(static fn ($columnName): string => trim($columnName, '`'), $keys);
12✔
159

160
                $allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
12✔
161
                    $hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
12✔
162

163
                    return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
12✔
164
                });
12✔
165

166
                foreach ($allIndexes as $index) {
12✔
167
                    $constraints = $index->fields;
11✔
168
                    break;
11✔
169
                }
170

171
                $constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
12✔
172
            }
173

174
            if (empty($constraints)) {
19✔
175
                if ($this->db->DBDebug) {
1✔
176
                    throw new DatabaseException('No constraint found for upsert.');
1✔
177
                }
178

179
                return ''; // @codeCoverageIgnore
180
            }
181

182
            $alias = $this->QBOptions['alias'] ?? '`excluded`';
18✔
183

184
            if (strtolower($alias) !== '`excluded`') {
18✔
UNCOV
185
                throw new InvalidArgumentException('SQLite alias is always named "excluded". A custom alias cannot be used.');
×
186
            }
187

188
            $updateFields = $this->QBOptions['updateFields'] ??
18✔
189
                $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
18✔
190
                [];
15✔
191

192
            $sql = 'INSERT INTO ' . $table . ' (';
18✔
193

194
            $sql .= implode(', ', array_map(static fn ($columnName): string => $columnName, $keys));
18✔
195

196
            $sql .= ")\n";
18✔
197

198
            $sql .= '{:_table_:}';
18✔
199

200
            $sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
18✔
201

202
            $sql .= "DO UPDATE SET\n";
18✔
203

204
            $sql .= implode(
18✔
205
                ",\n",
18✔
206
                array_map(
18✔
207
                    static fn ($key, $value): string => $key . ($value instanceof RawSql ?
18✔
208
                        " = {$value}" :
2✔
209
                        " = {$alias}.{$value}"),
18✔
210
                    array_keys($updateFields),
18✔
211
                    $updateFields,
18✔
212
                ),
18✔
213
            );
18✔
214

215
            $this->QBOptions['sql'] = $sql;
18✔
216
        }
217

218
        if (isset($this->QBOptions['setQueryAsData'])) {
18✔
219
            $hasWhere = stripos($this->QBOptions['setQueryAsData'], 'WHERE') > 0;
1✔
220

221
            $data = $this->QBOptions['setQueryAsData'] . ($hasWhere ? '' : "\nWHERE 1 = 1\n");
1✔
222
        } else {
223
            $data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
17✔
224
        }
225

226
        return str_replace('{:_table_:}', $data, $sql);
18✔
227
    }
228

229
    /**
230
     * Generates a platform-specific batch update string from the supplied data
231
     */
232
    protected function _deleteBatch(string $table, array $keys, array $values): string
233
    {
234
        $sql = $this->QBOptions['sql'] ?? '';
3✔
235

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

240
            if ($constraints === []) {
3✔
UNCOV
241
                if ($this->db->DBDebug) {
×
242
                    throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
243
                }
244

245
                return ''; // @codeCoverageIgnore
246
            }
247

248
            $sql = 'DELETE FROM ' . $table . "\n";
3✔
249

250
            if (current($constraints) instanceof RawSql && $this->db->DBDebug) {
3✔
UNCOV
251
                throw new DatabaseException('You cannot use RawSql for constraint in SQLite.');
×
252
                // @codeCoverageIgnore
253
            }
254

255
            if (is_string(current(array_keys($constraints)))) {
3✔
256
                $concat1 = implode(' || ', array_keys($constraints));
2✔
257
                $concat2 = implode(' || ', array_values($constraints));
2✔
258
            } else {
259
                $concat1 = implode(' || ', $constraints);
1✔
260
                $concat2 = $concat1;
1✔
261
            }
262

263
            $sql .= "WHERE {$concat1} IN (SELECT {$concat2} FROM (\n{:_table_:}))";
3✔
264

265
            // where is not supported
266
            if ($this->QBWhere !== [] && $this->db->DBDebug) {
3✔
UNCOV
267
                throw new DatabaseException('You cannot use WHERE with SQLite.');
×
268
                // @codeCoverageIgnore
269
            }
270

271
            $this->QBOptions['sql'] = $sql;
3✔
272
        }
273

274
        if (isset($this->QBOptions['setQueryAsData'])) {
3✔
275
            $data = $this->QBOptions['setQueryAsData'];
1✔
276
        } else {
277
            $data = implode(
2✔
278
                " UNION ALL\n",
2✔
279
                array_map(
2✔
280
                    static fn ($value): string => 'SELECT ' . implode(', ', array_map(
2✔
281
                        static fn ($key, $index): string => $index . ' ' . $key,
2✔
282
                        $keys,
2✔
283
                        $value,
2✔
284
                    )),
2✔
285
                    $values,
2✔
286
                ),
2✔
287
            ) . "\n";
2✔
288
        }
289

290
        return str_replace('{:_table_:}', $data, $sql);
3✔
291
    }
292
}
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