Completed
Pull Request — master (#23)
by Damian
08:37
created

code/SQLite3SchemaManager.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * SQLite schema manager class
5
 * 
6
 * @package SQLite3
7
 */
8
class SQLite3SchemaManager extends DBSchemaManager
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
9
{
10
11
    /**
12
     * Instance of the database controller this schema belongs to
13
     * 
14
     * @var SQLite3Database
15
     */
16
    protected $database = null;
17
18
    /**
19
     * Flag indicating whether or not the database has been checked and repaired
20
     *
21
     * @var boolean
22
     */
23
    protected static $checked_and_repaired = false;
24
25
    /**
26
     * Should schema be vacuumed during checkeAndRepairTable?
27
     *
28
     * @var boolean
29
     */
30
    public static $vacuum = true;
31
    
32
    public function createDatabase($name)
33
    {
34
        // Ensure that any existing database is cleared before connection
35
        $this->dropDatabase($name);
36
    }
37
38
    public function dropDatabase($name)
39
    {
40
        // No need to delete database files if operating purely within memory
41
        if ($this->database->getLivesInMemory()) {
42
            return;
43
        }
44
         
45
        // If using file based database ensure any existing file is removed
46
        $parameters = $this->database->getParameters();
47
        $fullpath = $parameters['path'] . '/' . $name;
48
        if (is_writable($fullpath)) {
49
            unlink($fullpath);
50
        }
51
    }
52
    
53
    public function databaseList()
54
    {
55
        $parameters = $this->database->getParameters();
56
        
57
        // If in-memory use the current database name only
58
        if ($this->database->getLivesInMemory()) {
59
            return array($parameters['database']);
60
        }
61
        
62
        // If using file based database enumerate files in the database directory
63
        $directory = $parameters['path'];
64
        $files = scandir($directory);
65
        
66
        // Filter each file in this directory
67
        $databases = array();
68
        if ($files !== false) {
69
            foreach ($files as $file) {
70
            
71
            // Filter non-files
72
            if (!is_file("$directory/$file")) {
73
                continue;
74
            }
75
            
76
            // Filter those with correct extension
77
            if (!SQLite3Database::is_valid_database_name($file)) {
78
                continue;
79
            }
80
            
81
                $databases[] = $file;
82
            }
83
        }
84
        return $databases;
85
    }
86
87
    public function databaseExists($name)
88
    {
89
        $databases = $this->databaseList();
90
        return in_array($name, $databases);
91
    }
92
    
93
    /**
94
     * Empties any cached enum values
95
     */
96
    public function flushCache()
97
    {
98
        $this->enum_map = array();
99
    }
100
    
101
    public function schemaUpdate($callback)
102
    {
103
        // Set locking mode
104
        $this->database->setPragma('locking_mode', 'EXCLUSIVE');
105
        $this->checkAndRepairTable();
106
        $this->flushCache();
107
        
108
        // Initiate schema update
109
        $error = null;
110
        try {
111
            parent::schemaUpdate($callback);
112
        } catch (Exception $ex) {
113
            $error = $ex;
114
        }
115
        
116
        // Revert locking mode
117
        $this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']);
118
        
119
        if ($error) {
120
            throw $error;
121
        }
122
    }
123
124
    /**
125
     * Empty a specific table
126
     * 
127
     * @param string $table
128
     */
129
    public function clearTable($table)
130
    {
131
        if ($table != 'SQLiteEnums') {
132
            $this->dbConn->query("DELETE FROM \"$table\"");
133
        }
134
    }
135
136
    public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
137
    {
138
        if (!isset($fields['ID'])) {
139
            $fields['ID'] = $this->IdColumn();
140
        }
141
142
        $fieldSchemata = array();
143
        if ($fields) {
144
            foreach ($fields as $k => $v) {
145
                $fieldSchemata[] = "\"$k\" $v";
146
            }
147
        }
148
        $fieldSchemas = implode(",\n", $fieldSchemata);
149
150
        // Switch to "CREATE TEMPORARY TABLE" for temporary tables
151
        $temporary = empty($options['temporary']) ? "" : "TEMPORARY";
152
        $this->query("CREATE $temporary TABLE \"$table\" (
153
			$fieldSchemas
154
		)");
155
156
        if ($indexes) {
157
            foreach ($indexes as $indexName => $indexDetails) {
158
                $this->createIndex($table, $indexName, $indexDetails);
159
            }
160
        }
161
162
        return $table;
163
    }
164
165
    public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null,
166
        $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null
167
    ) {
168
        if ($newFields) {
169
            foreach ($newFields as $fieldName => $fieldSpec) {
170
                $this->createField($tableName, $fieldName, $fieldSpec);
171
            }
172
        }
173
174
        if ($alteredFields) {
175
            foreach ($alteredFields as $fieldName => $fieldSpec) {
176
                $this->alterField($tableName, $fieldName, $fieldSpec);
177
            }
178
        }
179
180
        if ($newIndexes) {
181
            foreach ($newIndexes as $indexName => $indexSpec) {
182
                $this->createIndex($tableName, $indexName, $indexSpec);
183
            }
184
        }
185
186
        if ($alteredIndexes) {
187
            foreach ($alteredIndexes as $indexName => $indexSpec) {
188
                $this->alterIndex($tableName, $indexName, $indexSpec);
189
            }
190
        }
191
    }
192
    
193
    public function renameTable($oldTableName, $newTableName)
194
    {
195
        $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\"");
196
    }
197
198
    public function checkAndRepairTable($tableName = null)
199
    {
200
        $ok = true;
201
202
        if (!SapphireTest::using_temp_db() && !self::$checked_and_repaired) {
203
            $this->alterationMessage("Checking database integrity", "repaired");
204
            
205
            // Check for any tables with failed integrity
206
            if ($messages = $this->query('PRAGMA integrity_check')) {
207
                foreach ($messages as $message) {
208
                    if ($message['integrity_check'] != 'ok') {
209
                        Debug::show($message['integrity_check']);
210
                        $ok = false;
211
                    }
212
                }
213
            }
214
            
215
            // If enabled vacuum (clean and rebuild) the database
216
            if (self::$vacuum) {
217
                $this->query('VACUUM', E_USER_NOTICE);
218
                $message = $this->database->getConnector()->getLastError();
219
                if (preg_match('/authoriz/', $message)) {
220
                    $this->alterationMessage("VACUUM | $message", "error");
221
                } else {
222
                    $this->alterationMessage("VACUUMing", "repaired");
223
                }
224
            }
225
            self::$checked_and_repaired = true;
226
        }
227
        
228
        return $ok;
229
    }
230
231
    public function createField($table, $field, $spec)
232
    {
233
        $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec");
234
    }
235
236
    /**
237
     * Change the database type of the given field.
238
     * @param string $tableName The name of the tbale the field is in.
239
     * @param string $fieldName The name of the field to change.
240
     * @param string $fieldSpec The new field specification
241
     */
242
    public function alterField($tableName, $fieldName, $fieldSpec)
243
    {
244
        $oldFieldList = $this->fieldList($tableName);
245
        $fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"';
246
247
        if (!empty($_REQUEST['avoidConflict']) && Director::isDev()) {
248
            $fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec);
249
        }
250
251
        // Skip non-existing columns
252
        if (!array_key_exists($fieldName, $oldFieldList)) {
253
            return;
254
        }
255
256
        // Update field spec
257
        $newColsSpec = array();
258
        foreach ($oldFieldList as $name => $oldSpec) {
259
            $newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec);
260
        }
261
262
        $queries = array(
263
            "BEGIN TRANSACTION",
264
            "CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")",
265
            "INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"",
266
            "DROP TABLE \"$tableName\"",
267
            "ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"",
268
            "COMMIT"
269
        );
270
271
        // Remember original indexes
272
        $indexList = $this->indexList($tableName);
273
        
274
        // Then alter the table column
275
        foreach ($queries as $query) {
276
            $this->query($query.';');
277
        }
278
279
        // Recreate the indexes
280
        foreach ($indexList as $indexName => $indexSpec) {
281
            $this->createIndex($tableName, $indexName, $indexSpec);
282
        }
283
    }
284
285
    public function renameField($tableName, $oldName, $newName)
286
    {
287
        $oldFieldList = $this->fieldList($tableName);
288
289
        // Skip non-existing columns
290
        if (!array_key_exists($oldName, $oldFieldList)) {
291
            return;
292
        }
293
        
294
        // Determine column mappings
295
        $oldCols = array();
296
        $newColsSpec = array();
297
        foreach ($oldFieldList as $name => $spec) {
298
            $oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : '');
299
            $newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec";
300
        }
301
302
        // SQLite doesn't support direct renames through ALTER TABLE
303
        $queries = array(
304
            "BEGIN TRANSACTION",
305
            "CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")",
306
            "INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"",
307
            "DROP TABLE \"$tableName\"",
308
            "ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"",
309
            "COMMIT"
310
        );
311
312
        // Remember original indexes
313
        $oldIndexList = $this->indexList($tableName);
314
315
        // Then alter the table column
316
        foreach ($queries as $query) {
317
            $this->query($query.';');
318
        }
319
320
        // Recreate the indexes
321
        foreach ($oldIndexList as $indexName => $indexSpec) {
322
            // Rename columns to new columns
323
            $indexSpec['value'] = preg_replace("/\"$oldName\"/i", "\"$newName\"", $indexSpec['value']);
324
            $this->createIndex($tableName, $indexName, $indexSpec);
325
        }
326
    }
327
328
    public function fieldList($table)
329
    {
330
        $sqlCreate = $this->preparedQuery(
331
            'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?',
332
            array('table', $table)
333
        )->record();
334
        
335
        $fieldList = array();
336
        if ($sqlCreate && $sqlCreate['sql']) {
337
            preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims',
338
                $sqlCreate['sql'], $matches
339
            );
340
            $fields = isset($matches[1])
341
                ? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1])
342
                : array();
343
            foreach ($fields as $field) {
344
                $details = preg_split('/\s/', trim($field));
345
                $name = array_shift($details);
346
                $name = str_replace('"', '', trim($name));
347
                $fieldList[$name] = implode(' ', $details);
348
            }
349
        }
350
        return $fieldList;
351
    }
352
353
    /**
354
     * Create an index on a table.
355
     * 
356
     * @param string $tableName The name of the table.
357
     * @param string $indexName The name of the index.
358
     * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
359
     */
360
    public function createIndex($tableName, $indexName, $indexSpec)
361
    {
362
        $parsedSpec = $this->parseIndexSpec($indexName, $indexSpec);
363
        $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
364
        $columns = $parsedSpec['value'];
365
        $unique = ($parsedSpec['type'] == 'unique') ? 'UNIQUE' : '';
366
        $this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)");
367
    }
368
369
    public function alterIndex($tableName, $indexName, $indexSpec)
370
    {
371
        // Drop existing index
372
        $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
373
        $this->query("DROP INDEX IF EXISTS \"$sqliteName\"");
374
        
375
        // Create the index
376
        $this->createIndex($tableName, $indexName, $indexSpec);
377
    }
378
    
379
    /**
380
     * Builds the internal SQLLite index name given the silverstripe table and index name.
381
     * 
382
     * The name is built using the table and index name in order to prevent name collisions
383
     * between indexes of the same name across multiple tables
384
     * 
385
     * @param string $tableName
386
     * @param string $indexName
387
     * @return string The SQLite3 name of the index
388
     */
389
    protected function buildSQLiteIndexName($tableName, $indexName)
390
    {
391
        return "{$tableName}_{$indexName}";
392
    }
393
    
394
    protected function parseIndexSpec($name, $spec)
395
    {
396
        $spec = parent::parseIndexSpec($name, $spec);
397
        
398
        // Only allow index / unique index types
399
        if (!in_array($spec['type'], array('index', 'unique'))) {
400
            $spec['type'] = 'index';
401
        }
402
        
403
        return $spec;
404
    }
405
    
406
    public function indexKey($table, $index, $spec)
407
    {
408
        return $this->buildSQLiteIndexName($table, $index);
409
    }
410
411
    public function indexList($table)
412
    {
413
        $indexList = array();
414
        
415
        // Enumerate each index and related fields
416
        foreach ($this->query("PRAGMA index_list(\"$table\")") as $index) {
417
            
418
            // The SQLite internal index name, not the actual Silverstripe name
419
            $indexName = $index["name"];
420
            $indexType = $index['unique'] ? 'unique' : 'index';
421
            
422
            // Determine a clean list of column names within this index
423
            $list = array();
424
            foreach ($this->query("PRAGMA index_info(\"$indexName\")") as $details) {
425
                $list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']);
426
            }
427
            
428
            // Safely encode this spec
429
            $indexList[$indexName] = $this->parseIndexSpec($indexName, array(
430
                'name' => $indexName,
431
                'value' => $this->implodeColumnList($list),
432
                'type' => $indexType
433
            ));
434
        }
435
436
        return $indexList;
437
    }
438
439
    public function tableList()
440
    {
441
        $tables = array();
442
        $result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table'));
443
        foreach ($result as $record) {
444
            $table = reset($record);
445
            $tables[strtolower($table)] = $table;
446
        }
447
        return $tables;
448
    }
449
    
450
    /**
451
     * Return a boolean type-formatted string
452
     * 
453
     * @params array $values Contains a tokenised list of info about this data type
454
     * @return string
455
     */
456
    public function boolean($values)
457
    {
458
        $default = empty($values['default']) ? 0 : (int)$values['default'];
459
        return "BOOL NOT NULL DEFAULT $default";
460
    }
461
462
    /**
463
     * Return a date type-formatted string
464
     * 
465
     * @params array $values Contains a tokenised list of info about this data type
466
     * @return string
467
     */
468
    public function date($values)
469
    {
470
        return "TEXT";
471
    }
472
473
    /**
474
     * Return a decimal type-formatted string
475
     * 
476
     * @params array $values Contains a tokenised list of info about this data type
477
     * @return string
478
     */
479
    public function decimal($values, $asDbValue = false)
480
    {
481
        $default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0;
482
        return "NUMERIC NOT NULL DEFAULT $default";
483
    }
484
485
    /**
486
     * Cached list of enum values indexed by table.column
487
     *
488
     * @var array
489
     */
490
    protected $enum_map = array();
491
    
492
    /**
493
     * Return a enum type-formatted string
494
     *
495
     * enums are not supported. as a workaround to store allowed values we creates an additional table
496
     * 
497
     * @params array $values Contains a tokenised list of info about this data type
498
     * @return string
499
     */
500
    public function enum($values)
501
    {
502
        $tablefield = $values['table'] . '.' . $values['name'];
503
        $enumValues = implode(',', $values['enums']);
504
        
505
        // Ensure the cache table exists
506
        if (empty($this->enum_map)) {
507
            $this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)");
508
        }
509
        
510
        // Ensure the table row exists
511
        if (empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) {
512
            $this->preparedQuery(
513
                "REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)",
514
                array($tablefield, $enumValues)
515
            );
516
            $this->enum_map[$tablefield] = $enumValues;
517
        }
518
        
519
        // Set default
520
        if (!empty($values['default'])) {
521
            $default = str_replace(array('"', "'", "\\", "\0"), "", $values['default']);
522
            return "TEXT DEFAULT '$default'";
523
        } else {
524
            return 'TEXT';
525
        }
526
    }
527
    
528
    /**
529
     * Return a set type-formatted string
530
     * This type doesn't exist in SQLite either
531
     * 
532
     * @see SQLite3SchemaManager::enum()
533
     * 
534
     * @params array $values Contains a tokenised list of info about this data type
535
     * @return string
536
     */
537
    public function set($values)
538
    {
539
        return $this->enum($values);
540
    }
541
542
    /**
543
     * Return a float type-formatted string
544
     * 
545
     * @params array $values Contains a tokenised list of info about this data type
546
     * @return string
547
     */
548
    public function float($values, $asDbValue = false)
549
    {
550
        return "REAL";
551
    }
552
553
    /**
554
     * Return a Double type-formatted string
555
     * 
556
     * @params array $values Contains a tokenised list of info about this data type
557
     * @return string
558
     */
559
    public function double($values, $asDbValue = false)
560
    {
561
        return "REAL";
562
    }
563
564
    /**
565
     * Return a int type-formatted string
566
     * 
567
     * @params array $values Contains a tokenised list of info about this data type
568
     * @return string
569
     */
570
    public function int($values, $asDbValue = false)
571
    {
572
        return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default'];
573
    }
574
575
    /**
576
     * Return a bigint type-formatted string
577
     *
578
     * @params array $values Contains a tokenised list of info about this data type
579
     * @return string
580
     */
581
    public function bigint($values, $asDbValue = false)
582
    {
583
        return $this->int($values, $asDbValue);
584
    }
585
586
    /**
587
     * Return a datetime type-formatted string
588
     * For SQLite3, we simply return the word 'TEXT', no other parameters are necessary
589
     * 
590
     * @params array $values Contains a tokenised list of info about this data type
591
     * @return string
592
     */
593
    public function ss_datetime($values, $asDbValue = false)
594
    {
595
        return "DATETIME";
596
    }
597
598
    /**
599
     * Return a text type-formatted string
600
     * 
601
     * @params array $values Contains a tokenised list of info about this data type
602
     * @return string
603
     */
604
    public function text($values, $asDbValue = false)
605
    {
606
        return 'TEXT';
607
    }
608
609
    /**
610
     * Return a time type-formatted string
611
     * 
612
     * @params array $values Contains a tokenised list of info about this data type
613
     * @return string
614
     */
615
    public function time($values)
616
    {
617
        return "TEXT";
618
    }
619
620
    /**
621
     * Return a varchar type-formatted string
622
     * 
623
     * @params array $values Contains a tokenised list of info about this data type
624
     * @return string
625
     */
626
    public function varchar($values, $asDbValue = false)
627
    {
628
        return "VARCHAR({$values['precision']}) COLLATE NOCASE";
629
    }
630
631
    /*
632
     * Return a 4 digit numeric type.  MySQL has a proprietary 'Year' type.
633
     * For SQLite3 we use TEXT
634
     */
635
    public function year($values, $asDbValue = false)
636
    {
637
        return "TEXT";
638
    }
639
640
    public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
641
    {
642
        return 'INTEGER PRIMARY KEY AUTOINCREMENT';
643
    }
644
645
    public function hasTable($tableName)
646
    {
647
        return (bool)$this->preparedQuery(
648
            'SELECT name FROM sqlite_master WHERE type = ? AND name = ?',
649
            array('table', $tableName)
650
        )->first();
651
    }
652
653
    /**
654
     * Return enum values for the given field
655
     * 
656
     * @return array
657
     */
658
    public function enumValuesForField($tableName, $fieldName)
659
    {
660
        $tablefield = "$tableName.$fieldName";
661
        
662
        // Check already cached values for this field
663
        if (!empty($this->enum_map[$tablefield])) {
664
            return explode(',', $this->enum_map[$tablefield]);
665
        }
666
        
667
        // Retrieve and cache these details from the database
668
        $classnameinfo = $this->preparedQuery(
669
            "SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?",
670
            array($tablefield)
671
        )->first();
672
        if ($classnameinfo) {
673
            $valueList = $classnameinfo['EnumList'];
674
            $this->enum_map[$tablefield] = $valueList;
675
            return explode(',', $valueList);
676
        }
677
        
678
        // Fallback to empty list
679
        return array();
680
    }
681
    
682
    public function dbDataType($type)
683
    {
684
        $values = array(
685
            'unsigned integer' => 'INT'
686
        );
687
        
688
        if (isset($values[$type])) {
689
            return $values[$type];
690
        } else {
691
            return '';
692
        }
693
    }
694
}
695