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

codeigniter4 / CodeIgniter4 / 14569795065

21 Apr 2025 07:55AM UTC coverage: 84.402% (+0.007%) from 84.395%
14569795065

Pull #9528

github

web-flow
Merge 4ad1f19d8 into 3d3ba0512
Pull Request #9528: feat: add Time::addCalendarMonths() function

11 of 11 new or added lines in 1 file covered. (100.0%)

136 existing lines in 21 files now uncovered.

20827 of 24676 relevant lines covered (84.4%)

191.03 hits per line

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

97.45
/system/Database/SQLite3/Table.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\Exceptions\DataException;
17
use stdClass;
18

19
/**
20
 * Provides missing features for altering tables that are common
21
 * in other supported databases, but are missing from SQLite.
22
 * These are needed in order to support migrations during testing
23
 * when another database is used as the primary engine, but
24
 * SQLite in memory databases are used for faster test execution.
25
 */
26
class Table
27
{
28
    /**
29
     * All of the fields this table represents.
30
     *
31
     * @var array<string, array<string, bool|int|string|null>> [name => attributes]
32
     */
33
    protected $fields = [];
34

35
    /**
36
     * All of the unique/primary keys in the table.
37
     *
38
     * @var array
39
     */
40
    protected $keys = [];
41

42
    /**
43
     * All of the foreign keys in the table.
44
     *
45
     * @var array
46
     */
47
    protected $foreignKeys = [];
48

49
    /**
50
     * The name of the table we're working with.
51
     *
52
     * @var string
53
     */
54
    protected $tableName;
55

56
    /**
57
     * The name of the table, with database prefix
58
     *
59
     * @var string
60
     */
61
    protected $prefixedTableName;
62

63
    /**
64
     * Database connection.
65
     *
66
     * @var Connection
67
     */
68
    protected $db;
69

70
    /**
71
     * Handle to our forge.
72
     *
73
     * @var Forge
74
     */
75
    protected $forge;
76

77
    /**
78
     * Table constructor.
79
     */
80
    public function __construct(Connection $db, Forge $forge)
81
    {
82
        $this->db    = $db;
32✔
83
        $this->forge = $forge;
32✔
84
    }
85

86
    /**
87
     * Reads an existing database table and
88
     * collects all of the information needed to
89
     * recreate this table.
90
     *
91
     * @return Table
92
     */
93
    public function fromTable(string $table)
94
    {
95
        $this->prefixedTableName = $table;
32✔
96

97
        $prefix = $this->db->DBPrefix;
32✔
98

99
        if (! empty($prefix) && str_starts_with($table, $prefix)) {
32✔
100
            $table = substr($table, strlen($prefix));
24✔
101
        }
102

103
        if (! $this->db->tableExists($this->prefixedTableName)) {
32✔
104
            throw DataException::forTableNotFound($this->prefixedTableName);
1✔
105
        }
106

107
        $this->tableName = $table;
31✔
108

109
        $this->fields = $this->formatFields($this->db->getFieldData($table));
31✔
110

111
        $this->keys = array_merge($this->keys, $this->formatKeys($this->db->getIndexData($table)));
31✔
112

113
        // if primary key index exists twice then remove psuedo index name 'primary'.
114
        $primaryIndexes = array_filter($this->keys, static fn ($index): bool => $index['type'] === 'primary');
31✔
115

116
        if ($primaryIndexes !== [] && count($primaryIndexes) > 1 && array_key_exists('primary', $this->keys)) {
31✔
117
            unset($this->keys['primary']);
2✔
118
        }
119

120
        $this->foreignKeys = $this->db->getForeignKeyData($table);
31✔
121

122
        return $this;
31✔
123
    }
124

125
    /**
126
     * Called after `fromTable` and any actions, like `dropColumn`, etc,
127
     * to finalize the action. It creates a temp table, creates the new
128
     * table with modifications, and copies the data over to the new table.
129
     * Resets the connection dataCache to be sure changes are collected.
130
     */
131
    public function run(): bool
132
    {
133
        $this->db->query('PRAGMA foreign_keys = OFF');
30✔
134

135
        $this->db->transStart();
30✔
136

137
        $this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
30✔
138

139
        $this->forge->reset();
30✔
140

141
        $this->createTable();
30✔
142

143
        $this->copyData();
30✔
144

145
        $this->forge->dropTable("temp_{$this->tableName}");
30✔
146

147
        $success = $this->db->transComplete();
30✔
148

149
        $this->db->query('PRAGMA foreign_keys = ON');
30✔
150

151
        $this->db->resetDataCache();
30✔
152

153
        return $success;
30✔
154
    }
155

156
    /**
157
     * Drops columns from the table.
158
     *
159
     * @param list<string>|string $columns Column names to drop.
160
     *
161
     * @return Table
162
     */
163
    public function dropColumn($columns)
164
    {
165
        if (is_string($columns)) {
18✔
166
            $columns = explode(',', $columns);
3✔
167
        }
168

169
        foreach ($columns as $column) {
18✔
170
            $column = trim($column);
18✔
171
            if (isset($this->fields[$column])) {
18✔
172
                unset($this->fields[$column]);
18✔
173
            }
174
        }
175

176
        return $this;
18✔
177
    }
178

179
    /**
180
     * Modifies a field, including changing data type, renaming, etc.
181
     *
182
     * @param list<array<string, bool|int|string|null>> $fieldsToModify
183
     *
184
     * @return Table
185
     */
186
    public function modifyColumn(array $fieldsToModify)
187
    {
188
        foreach ($fieldsToModify as $field) {
5✔
189
            $oldName = $field['name'];
5✔
190
            unset($field['name']);
5✔
191

192
            $this->fields[$oldName] = $field;
5✔
193
        }
194

195
        return $this;
5✔
196
    }
197

198
    /**
199
     * Drops the primary key
200
     */
201
    public function dropPrimaryKey(): Table
202
    {
203
        $primaryIndexes = array_filter($this->keys, static fn ($index): bool => strtolower($index['type']) === 'primary');
2✔
204

205
        foreach (array_keys($primaryIndexes) as $key) {
2✔
206
            unset($this->keys[$key]);
2✔
207
        }
208

209
        return $this;
2✔
210
    }
211

212
    /**
213
     * Drops a foreign key from this table so that
214
     * it won't be recreated in the future.
215
     *
216
     * @return Table
217
     */
218
    public function dropForeignKey(string $foreignName)
219
    {
220
        if (empty($this->foreignKeys)) {
2✔
UNCOV
221
            return $this;
×
222
        }
223

224
        if (isset($this->foreignKeys[$foreignName])) {
2✔
225
            unset($this->foreignKeys[$foreignName]);
2✔
226
        }
227

228
        return $this;
2✔
229
    }
230

231
    /**
232
     * Adds primary key
233
     */
234
    public function addPrimaryKey(array $fields): Table
235
    {
236
        $primaryIndexes = array_filter($this->keys, static fn ($index): bool => strtolower($index['type']) === 'primary');
2✔
237

238
        // if primary key already exists we can't add another one
239
        if ($primaryIndexes !== []) {
2✔
UNCOV
240
            return $this;
×
241
        }
242

243
        // add array to keys of fields
244
        $pk = [
2✔
245
            'fields' => $fields['fields'],
2✔
246
            'type'   => 'primary',
2✔
247
        ];
2✔
248

249
        $this->keys['primary'] = $pk;
2✔
250

251
        return $this;
2✔
252
    }
253

254
    /**
255
     * Add a foreign key
256
     *
257
     * @return $this
258
     */
259
    public function addForeignKey(array $foreignKeys)
260
    {
261
        $fk = [];
2✔
262

263
        // convert to object
264
        foreach ($foreignKeys as $row) {
2✔
265
            $obj                      = new stdClass();
2✔
266
            $obj->column_name         = $row['field'];
2✔
267
            $obj->foreign_table_name  = $row['referenceTable'];
2✔
268
            $obj->foreign_column_name = $row['referenceField'];
2✔
269
            $obj->on_delete           = $row['onDelete'];
2✔
270
            $obj->on_update           = $row['onUpdate'];
2✔
271

272
            $fk[] = $obj;
2✔
273
        }
274

275
        $this->foreignKeys = array_merge($this->foreignKeys, $fk);
2✔
276

277
        return $this;
2✔
278
    }
279

280
    /**
281
     * Creates the new table based on our current fields.
282
     *
283
     * @return bool
284
     */
285
    protected function createTable()
286
    {
287
        $this->dropIndexes();
30✔
288
        $this->db->resetDataCache();
30✔
289

290
        // Handle any modified columns.
291
        $fields = [];
30✔
292

293
        foreach ($this->fields as $name => $field) {
30✔
294
            if (isset($field['new_name'])) {
30✔
295
                $fields[$field['new_name']] = $field;
3✔
296

297
                continue;
3✔
298
            }
299

300
            $fields[$name] = $field;
30✔
301
        }
302

303
        $this->forge->addField($fields);
30✔
304

305
        $fieldNames = array_keys($fields);
30✔
306

307
        $this->keys = array_filter(
30✔
308
            $this->keys,
30✔
309
            static fn ($index): bool => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields']),
30✔
310
        );
30✔
311

312
        // Unique/Index keys
313
        if (is_array($this->keys)) {
30✔
314
            foreach ($this->keys as $keyName => $key) {
30✔
315
                switch ($key['type']) {
14✔
316
                    case 'primary':
14✔
317
                        $this->forge->addPrimaryKey($key['fields']);
13✔
318
                        break;
13✔
319

320
                    case 'unique':
8✔
321
                        $this->forge->addUniqueKey($key['fields'], $keyName);
7✔
322
                        break;
7✔
323

324
                    case 'index':
4✔
325
                        $this->forge->addKey($key['fields'], false, false, $keyName);
4✔
326
                        break;
4✔
327
                }
328
            }
329
        }
330

331
        foreach ($this->foreignKeys as $foreignKey) {
30✔
332
            $this->forge->addForeignKey(
6✔
333
                $foreignKey->column_name,
6✔
334
                trim($foreignKey->foreign_table_name, $this->db->DBPrefix),
6✔
335
                $foreignKey->foreign_column_name,
6✔
336
            );
6✔
337
        }
338

339
        return $this->forge->createTable($this->tableName);
30✔
340
    }
341

342
    /**
343
     * Copies data from our old table to the new one,
344
     * taking care map data correctly based on any columns
345
     * that have been renamed.
346
     *
347
     * @return void
348
     */
349
    protected function copyData()
350
    {
351
        $exFields  = [];
30✔
352
        $newFields = [];
30✔
353

354
        foreach ($this->fields as $name => $details) {
30✔
355
            $newFields[] = $details['new_name'] ?? $name;
30✔
356
            $exFields[]  = $name;
30✔
357
        }
358

359
        $exFields = implode(
30✔
360
            ', ',
30✔
361
            array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields),
30✔
362
        );
30✔
363
        $newFields = implode(
30✔
364
            ', ',
30✔
365
            array_map(fn ($item) => $this->db->protectIdentifiers($item), $newFields),
30✔
366
        );
30✔
367

368
        $this->db->query(
30✔
369
            "INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}",
30✔
370
        );
30✔
371
    }
372

373
    /**
374
     * Converts fields retrieved from the database to
375
     * the format needed for creating fields with Forge.
376
     *
377
     * @param array|bool $fields
378
     *
379
     * @return         mixed
380
     * @phpstan-return ($fields is array ? array : mixed)
381
     */
382
    protected function formatFields($fields)
383
    {
384
        if (! is_array($fields)) {
31✔
385
            return $fields;
×
386
        }
387

388
        $return = [];
31✔
389

390
        foreach ($fields as $field) {
31✔
391
            $return[$field->name] = [
31✔
392
                'type'    => $field->type,
31✔
393
                'default' => $field->default,
31✔
394
                'null'    => $field->nullable,
31✔
395
            ];
31✔
396

397
            if ($field->default === null) {
31✔
398
                // `null` means that the default value is not defined.
399
                unset($return[$field->name]['default']);
31✔
400
            } elseif ($field->default === 'NULL') {
2✔
401
                // 'NULL' means that the default value is NULL.
402
                $return[$field->name]['default'] = null;
×
403
            } else {
404
                $default = trim($field->default, "'");
2✔
405

406
                if ($this->isIntegerType($field->type)) {
2✔
407
                    $default = (int) $default;
1✔
408
                } elseif ($this->isNumericType($field->type)) {
2✔
409
                    $default = (float) $default;
1✔
410
                }
411

412
                $return[$field->name]['default'] = $default;
2✔
413
            }
414

415
            if ($field->primary_key) {
31✔
416
                $this->keys['primary'] = [
17✔
417
                    'fields' => [$field->name],
17✔
418
                    'type'   => 'primary',
17✔
419
                ];
17✔
420
            }
421
        }
422

423
        return $return;
31✔
424
    }
425

426
    /**
427
     * Is INTEGER type?
428
     *
429
     * @param string $type SQLite data type (case-insensitive)
430
     *
431
     * @see https://www.sqlite.org/datatype3.html
432
     */
433
    private function isIntegerType(string $type): bool
434
    {
435
        return str_contains(strtoupper($type), 'INT');
2✔
436
    }
437

438
    /**
439
     * Is NUMERIC type?
440
     *
441
     * @param string $type SQLite data type (case-insensitive)
442
     *
443
     * @see https://www.sqlite.org/datatype3.html
444
     */
445
    private function isNumericType(string $type): bool
446
    {
447
        return in_array(strtoupper($type), ['NUMERIC', 'DECIMAL'], true);
2✔
448
    }
449

450
    /**
451
     * Converts keys retrieved from the database to
452
     * the format needed to create later.
453
     *
454
     * @param array<string, stdClass> $keys
455
     *
456
     * @return array<string, array{fields: string, type: string}>
457
     */
458
    protected function formatKeys($keys)
459
    {
460
        $return = [];
31✔
461

462
        foreach ($keys as $name => $key) {
31✔
463
            $return[strtolower($name)] = [
17✔
464
                'fields' => $key->fields,
17✔
465
                'type'   => strtolower($key->type),
17✔
466
            ];
17✔
467
        }
468

469
        return $return;
31✔
470
    }
471

472
    /**
473
     * Attempts to drop all indexes and constraints
474
     * from the database for this table.
475
     *
476
     * @return void
477
     */
478
    protected function dropIndexes()
479
    {
480
        if (! is_array($this->keys) || $this->keys === []) {
30✔
481
            return;
14✔
482
        }
483

484
        foreach (array_keys($this->keys) as $name) {
16✔
485
            if ($name === 'primary') {
16✔
486
                continue;
15✔
487
            }
488

489
            $this->db->query("DROP INDEX IF EXISTS '{$name}'");
9✔
490
        }
491
    }
492
}
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