Completed
Pull Request — master (#78)
by Damian
02:47
created

PostgreSQLSchemaManager::bigint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\PostgreSQL;
4
5
use SilverStripe\Dev\Deprecation;
6
use SilverStripe\ORM\Connect\DBSchemaManager;
7
use SilverStripe\ORM\DB;
8
9
/**
10
 * PostgreSQL schema manager
11
 *
12
 * @package sapphire
13
 * @subpackage model
14
 */
15
class PostgreSQLSchemaManager extends DBSchemaManager
16
{
17
    /**
18
     * Identifier for this schema, used for configuring schema-specific table
19
     * creation options
20
     */
21
    const ID = 'PostgreSQL';
22
23
    /**
24
     * Instance of the database controller this schema belongs to
25
     *
26
     * @var PostgreSQLDatabase
27
     */
28
    protected $database = null;
29
30
    /**
31
     * This holds a copy of all the constraint results that are returned
32
     * via the function constraintExists().  This is a bit faster than
33
     * repeatedly querying this column, and should allow the database
34
     * to use it's built-in caching features for better queries.
35
     *
36
     * @var array
37
     */
38
    protected static $cached_constraints = array();
39
40
    /**
41
     *
42
     * This holds a copy of all the queries that run through the function fieldList()
43
     * This is one of the most-often called functions, and repeats itself a great deal in the unit tests.
44
     *
45
     * @var array
46
     */
47
    protected static $cached_fieldlists = array();
48
49
    protected function indexKey($table, $index, $spec)
50
    {
51
        return $this->buildPostgresIndexName($table, $index);
52
    }
53
54
    /**
55
     * Creates a postgres database, ignoring model_schema_as_database
56
     *
57
     * @param string $name
58
     */
59
    public function createPostgresDatabase($name)
60
    {
61
        $this->query("CREATE DATABASE \"$name\";");
62
    }
63
64
    public function createDatabase($name)
65
    {
66
        if (PostgreSQLDatabase::model_schema_as_database()) {
67
            $schemaName = $this->database->databaseToSchemaName($name);
68
            return $this->createSchema($schemaName);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->createSchema($schemaName) targeting SilverStripe\PostgreSQL\...Manager::createSchema() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
69
        }
70
        return $this->createPostgresDatabase($name);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->createPostgresDatabase($name) targeting SilverStripe\PostgreSQL\...reatePostgresDatabase() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
71
    }
72
73
    /**
74
     * Determines if a postgres database exists, ignoring model_schema_as_database
75
     *
76
     * @param string $name
77
     * @return boolean
78
     */
79
    public function postgresDatabaseExists($name)
80
    {
81
        $result = $this->preparedQuery("SELECT datname FROM pg_database WHERE datname = ?;", array($name));
82
        return $result->first() ? true : false;
83
    }
84
85
    public function databaseExists($name)
86
    {
87
        if (PostgreSQLDatabase::model_schema_as_database()) {
88
            $schemaName = $this->database->databaseToSchemaName($name);
89
            return $this->schemaExists($schemaName);
90
        }
91
        return $this->postgresDatabaseExists($name);
92
    }
93
94
    /**
95
     * Determines the list of all postgres databases, ignoring model_schema_as_database
96
     *
97
     * @return array
98
     */
99
    public function postgresDatabaseList()
100
    {
101
        return $this->query("SELECT datname FROM pg_database WHERE datistemplate=false;")->column();
102
    }
103
104
    public function databaseList()
105
    {
106
        if (PostgreSQLDatabase::model_schema_as_database()) {
107
            $schemas = $this->schemaList();
108
            $names = array();
109
            foreach ($schemas as $schema) {
110
                $names[] = $this->database->schemaToDatabaseName($schema);
111
            }
112
            return array_unique($names);
113
        }
114
        return $this->postgresDatabaseList();
115
    }
116
117
    /**
118
     * Drops a postgres database, ignoring model_schema_as_database
119
     *
120
     * @param string $name
121
     */
122
    public function dropPostgresDatabase($name)
123
    {
124
        $nameSQL = $this->database->escapeIdentifier($name);
125
        $this->query("DROP DATABASE $nameSQL;");
126
    }
127
128
    public function dropDatabase($name)
129
    {
130
        if (PostgreSQLDatabase::model_schema_as_database()) {
131
            $schemaName = $this->database->databaseToSchemaName($name);
132
            $this->dropSchema($schemaName);
133
            return;
134
        }
135
        $this->dropPostgresDatabase($name);
136
    }
137
138
    /**
139
     * Returns true if the schema exists in the current database
140
     *
141
     * @param string $name
142
     * @return boolean
143
     */
144
    public function schemaExists($name)
145
    {
146
        return $this->preparedQuery(
147
            "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ?;",
148
            array($name)
149
        )->first() ? true : false;
150
    }
151
152
    /**
153
     * Creates a schema in the current database
154
     *
155
     * @param string $name
156
     */
157
    public function createSchema($name)
158
    {
159
        $nameSQL = $this->database->escapeIdentifier($name);
160
        $this->query("CREATE SCHEMA $nameSQL;");
161
    }
162
163
    /**
164
     * Drops a schema from the database. Use carefully!
165
     *
166
     * @param string $name
167
     */
168
    public function dropSchema($name)
169
    {
170
        $nameSQL = $this->database->escapeIdentifier($name);
171
        $this->query("DROP SCHEMA $nameSQL CASCADE;");
172
    }
173
174
    /**
175
     * Returns the list of all available schemas on the current database
176
     *
177
     * @return array
178
     */
179
    public function schemaList()
180
    {
181
        return $this->query(
182
            "SELECT nspname
183
            FROM pg_catalog.pg_namespace
184
            WHERE nspname <> 'information_schema' AND nspname !~ E'^pg_'"
185
        )->column();
186
    }
187
188
    public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
189
    {
190
        $fieldSchemas = "";
191
        if ($fields) {
192
            foreach ($fields as $k => $v) {
193
                $fieldSchemas .= "\"$k\" $v,\n";
194
            }
195
        }
196
        if (!empty($options[self::ID])) {
197
            $addOptions = $options[self::ID];
198
        } else {
199
            $addOptions = null;
200
        }
201
202
        //First of all, does this table already exist
203
        $doesExist = $this->hasTable($table);
204
        if ($doesExist) {
205
            // Table already exists, just return the name, in line with baseclass documentation.
206
            return $table;
207
        }
208
209
        //If we have a fulltext search request, then we need to create a special column
210
        //for GiST searches
211
        $fulltexts = '';
212
        $triggers = [];
213
        if ($indexes) {
214
            foreach ($indexes as $name => $this_index) {
215
                if (is_array($this_index) && $this_index['type'] == 'fulltext') {
216
                    $ts_details = $this->fulltext($this_index, $table, $name);
217
                    $fulltexts .= $ts_details['fulltexts'] . ', ';
218
                    $triggers[] = $ts_details['triggers'];
219
                }
220
            }
221
        }
222
223
        $indexQueries = [];
224
        if ($indexes) {
225
            foreach ($indexes as $k => $v) {
226
                $indexQueries[] = $this->getIndexSqlDefinition($table, $k, $v);
227
            }
228
        }
229
230
        //Do we need to create a tablespace for this item?
231 View Code Duplication
        if ($advancedOptions && isset($advancedOptions['tablespace'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
232
            $this->createOrReplaceTablespace(
233
                $advancedOptions['tablespace']['name'],
234
                $advancedOptions['tablespace']['location']
235
            );
236
            $tableSpace = ' TABLESPACE ' . $advancedOptions['tablespace']['name'];
237
        } else {
238
            $tableSpace = '';
239
        }
240
241
        $this->query(
242
            "CREATE TABLE \"$table\" (
243
                $fieldSchemas
244
                $fulltexts
245
                primary key (\"ID\")
246
            )$tableSpace $addOptions"
247
        );
248
        foreach ($indexQueries as $indexQuery) {
249
            $this->query($indexQuery);
250
        }
251
252
        foreach ($triggers as $trigger) {
253
            $this->query($trigger);
254
        }
255
256
        //If we have a partitioning requirement, we do that here:
257
        if ($advancedOptions && isset($advancedOptions['partitions'])) {
258
            $this->createOrReplacePartition($table, $advancedOptions['partitions'], $indexes, $advancedOptions);
259
        }
260
261
        //Lastly, clustering goes here:
262
        if ($advancedOptions && isset($advancedOptions['cluster'])) {
263
            $this->query("CLUSTER \"$table\" USING \"{$advancedOptions['cluster']}\"");
264
        }
265
266
        return $table;
267
    }
268
269
    /**
270
     * Builds the internal Postgres index name given the silverstripe table and index name
271
     *
272
     * @param string $tableName
273
     * @param string $indexName
274
     * @param string $prefix The optional prefix for the index. Defaults to "ix" for indexes.
275
     * @return string The postgres name of the index
276
     */
277
    protected function buildPostgresIndexName($tableName, $indexName, $prefix = 'ix')
278
    {
279
280
        // Assume all indexes also contain the table name
281
        // MD5 the table/index name combo to keep it to a fixed length.
282
        // Exclude the prefix so that the trigger name can be easily generated from the index name
283
        $indexNamePG = "{$prefix}_" . md5("{$tableName}_{$indexName}");
284
285
        // Limit to 63 characters
286
        if (strlen($indexNamePG) > 63) {
287
            return substr($indexNamePG, 0, 63);
288
        } else {
289
            return $indexNamePG;
290
        }
291
    }
292
293
    /**
294
     * Builds the internal Postgres trigger name given the silverstripe table and trigger name
295
     *
296
     * @param string $tableName
297
     * @param string $triggerName
298
     * @return string The postgres name of the trigger
299
     */
300
    public function buildPostgresTriggerName($tableName, $triggerName)
301
    {
302
        // Kind of cheating, but behaves the same way as indexes
303
        return $this->buildPostgresIndexName($tableName, $triggerName, 'ts');
304
    }
305
306
    public function alterTable(
307
        $table,
308
        $newFields = null,
309
        $newIndexes = null,
310
        $alteredFields = null,
311
        $alteredIndexes = null,
312
        $alteredOptions = null,
313
        $advancedOptions = null
314
    ) {
315
        $alterList = [];
316
        if ($newFields) {
317
            foreach ($newFields as $fieldName => $fieldSpec) {
318
                $alterList[] = "ADD \"$fieldName\" $fieldSpec";
319
            }
320
        }
321
322
        if ($alteredFields) {
323
            foreach ($alteredFields as $indexName => $indexSpec) {
324
                $val = $this->alterTableAlterColumn($table, $indexName, $indexSpec);
325
                if (!empty($val)) {
326
                    $alterList[] = $val;
327
                }
328
            }
329
        }
330
331
        //Do we need to do anything with the tablespaces?
332 View Code Duplication
        if ($alteredOptions && isset($advancedOptions['tablespace'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333
            $this->createOrReplaceTablespace(
334
                $advancedOptions['tablespace']['name'],
335
                $advancedOptions['tablespace']['location']
336
            );
337
            $this->query("ALTER TABLE \"$table\" SET TABLESPACE {$advancedOptions['tablespace']['name']};");
338
        }
339
340
        //DB ABSTRACTION: we need to change the constraints to be a separate 'add' command,
341
        //see http://www.postgresql.org/docs/8.1/static/sql-altertable.html
342
        $alterIndexList = [];
343
        //Pick up the altered indexes here:
344
        $fieldList = $this->fieldList($table);
345
        $fulltexts = [];
346
        $dropTriggers = [];
347
        $triggers = [];
348
        if ($alteredIndexes) {
349
            foreach ($alteredIndexes as $indexName => $indexSpec) {
350
                $indexNamePG = $this->buildPostgresIndexName($table, $indexName);
351
352
                if ($indexSpec['type'] == 'fulltext') {
353
                    //For full text indexes, we need to drop the trigger, drop the index, AND drop the column
354
355
                    //Go and get the tsearch details:
356
                    $ts_details = $this->fulltext($indexSpec, $table, $indexName);
357
358
                    //Drop this column if it already exists:
359
360
                    //No IF EXISTS option is available for Postgres <9.0
361
                    if (array_key_exists($ts_details['ts_name'], $fieldList)) {
362
                        $fulltexts[] = "ALTER TABLE \"{$table}\" DROP COLUMN \"{$ts_details['ts_name']}\";";
363
                    }
364
365
                    // We'll execute these later:
366
                    $triggerNamePG = $this->buildPostgresTriggerName($table, $indexName);
367
                    $dropTriggers[] = "DROP TRIGGER IF EXISTS \"$triggerNamePG\" ON \"$table\";";
368
                    $fulltexts[] = "ALTER TABLE \"{$table}\" ADD COLUMN {$ts_details['fulltexts']};";
369
                    $triggers[] = $ts_details['triggers'];
370
                }
371
372
                // Create index action (including fulltext)
373
                $alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";";
374
                $createIndex = $this->getIndexSqlDefinition($table, $indexName, $indexSpec);
375
                if ($createIndex) {
376
                    $alterIndexList[] = $createIndex;
377
                }
378
            }
379
        }
380
381
        //Add the new indexes:
382
        if ($newIndexes) {
383
            foreach ($newIndexes as $indexName => $indexSpec) {
384
                $indexNamePG = $this->buildPostgresIndexName($table, $indexName);
385
                //If we have a fulltext search request, then we need to create a special column
386
                //for GiST searches
387
                //Pick up the new indexes here:
388
                if ($indexSpec['type'] == 'fulltext') {
389
                    $ts_details = $this->fulltext($indexSpec, $table, $indexName);
390
                    if (!isset($fieldList[$ts_details['ts_name']])) {
391
                        $fulltexts[] = "ALTER TABLE \"{$table}\" ADD COLUMN {$ts_details['fulltexts']};";
392
                        $triggers[] = $ts_details['triggers'];
393
                    }
394
                }
395
396
                //Check that this index doesn't already exist:
397
                $indexes = $this->indexList($table);
398
                if (isset($indexes[$indexName])) {
399
                    $alterIndexList[] = "DROP INDEX IF EXISTS \"$indexNamePG\";";
400
                }
401
402
                $createIndex = $this->getIndexSqlDefinition($table, $indexName, $indexSpec);
403
                if ($createIndex) {
404
                    $alterIndexList[] = $createIndex;
405
                }
406
            }
407
        }
408
409
        if ($alterList) {
410
            $alterations = implode(",\n", $alterList);
411
            $this->query("ALTER TABLE \"$table\" " . $alterations);
412
        }
413
414
        //Do we need to create a tablespace for this item?
415 View Code Duplication
        if ($advancedOptions && isset($advancedOptions['extensions']['tablespace'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
416
            $extensions = $advancedOptions['extensions'];
417
            $this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']);
418
        }
419
420
        if ($alteredOptions && isset($this->class) && isset($alteredOptions[$this->class])) {
0 ignored issues
show
Bug Best Practice introduced by
The property class does not exist on SilverStripe\PostgreSQL\PostgreSQLSchemaManager. Did you maybe forget to declare it?
Loading history...
421
            $this->query(sprintf("ALTER TABLE \"%s\" %s", $table, $alteredOptions[$this->class]));
422
            DB::alteration_message(
423
                sprintf("Table %s options changed: %s", $table, $alteredOptions[$this->class]),
424
                "changed"
425
            );
426
        }
427
428
        //Create any fulltext columns and triggers here:
429
        foreach ($fulltexts as $fulltext) {
430
            $this->query($fulltext);
431
        }
432
        foreach ($dropTriggers as $dropTrigger) {
433
            $this->query($dropTrigger);
434
        }
435
436
        foreach ($triggers as $trigger) {
437
            $this->query($trigger);
438
            $triggerFields = $this->triggerFieldsFromTrigger($trigger);
439
            if ($triggerFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $triggerFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
440
                //We need to run a simple query to force the database to update the triggered columns
441
                $this->query("UPDATE \"{$table}\" SET \"{$triggerFields[0]}\"=\"$triggerFields[0]\";");
442
            }
443
        }
444
445
        foreach ($alterIndexList as $alteration) {
446
            $this->query($alteration);
447
        }
448
449
        //If we have a partitioning requirement, we do that here:
450
        if ($advancedOptions && isset($advancedOptions['partitions'])) {
451
            $this->createOrReplacePartition($table, $advancedOptions['partitions']);
452
        }
453
454
        //Lastly, clustering goes here:
455
        if ($advancedOptions && isset($advancedOptions['cluster'])) {
456
            $clusterIndex = $this->buildPostgresIndexName($table, $advancedOptions['cluster']);
457
            $this->query("CLUSTER \"$table\" USING \"$clusterIndex\";");
458
        } else {
459
            //Check that clustering is not on this table, and if it is, remove it:
460
461
            //This is really annoying.  We need the oid of this table:
462
            $stats = $this->preparedQuery(
463
                "SELECT relid FROM pg_stat_user_tables WHERE relname = ?;",
464
                array($table)
465
            )->first();
466
            $oid = $stats['relid'];
467
468
            //Now we can run a long query to get the clustered status:
469
            //If anyone knows a better way to get the clustered status, then feel free to replace this!
470
            $clustered = $this->preparedQuery(
471
                "
472
                SELECT c2.relname, i.indisclustered 
473
                FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i
474
                WHERE c.oid = ? AND c.oid = i.indrelid AND i.indexrelid = c2.oid AND indisclustered='t';",
475
                array($oid)
476
            )->first();
477
478
            if ($clustered) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $clustered of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
479
                $this->query("ALTER TABLE \"$table\" SET WITHOUT CLUSTER;");
480
            }
481
        }
482
    }
483
484
    /*
485
     * Creates an ALTER expression for a column in PostgreSQL
486
     *
487
     * @param $tableName Name of the table to be altered
0 ignored issues
show
Bug introduced by
The type SilverStripe\PostgreSQL\Name was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
488
     * @param $colName   Name of the column to be altered
489
     * @param $colSpec   String which contains conditions for a column
490
     * @return string
491
     */
492
    private function alterTableAlterColumn($tableName, $colName, $colSpec)
493
    {
494
        // First, we split the column specifications into parts
495
        // TODO: this returns an empty array for the following string: int(11) not null auto_increment
496
        //         on second thoughts, why is an auto_increment field being passed through?
497
498
        $pattern = '/^([\w(\,)]+)\s?((?:not\s)?null)?\s?(default\s[\w\.\']+)?\s?(check\s[\w()\'",\s]+)?$/i';
499
        preg_match($pattern, $colSpec, $matches);
500
501
        if (sizeof($matches) == 0) {
502
            return '';
503
        }
504
505
        if ($matches[1] == 'serial8') {
506
            return '';
507
        }
508
509
        if (isset($matches[1])) {
510
            $alterCol = "ALTER COLUMN \"$colName\" TYPE $matches[1] USING \"$colName\"::$matches[1]\n";
511
512
            // SET null / not null
513
            if (!empty($matches[2])) {
514
                $alterCol .= ",\nALTER COLUMN \"$colName\" SET $matches[2]";
515
            }
516
517
            // SET default (we drop it first, for reasons of precaution)
518
            if (!empty($matches[3])) {
519
                $alterCol .= ",\nALTER COLUMN \"$colName\" DROP DEFAULT";
520
                $alterCol .= ",\nALTER COLUMN \"$colName\" SET $matches[3]";
521
            }
522
523
            // SET check constraint (The constraint HAS to be dropped)
524
            $constraintName = "{$tableName}_{$colName}_check";
525
            $constraintExists = $this->constraintExists($constraintName, false);
526
            if (isset($matches[4])) {
527
                //Take this new constraint and see what's outstanding from the target table:
528
                $constraint_bits = explode('(', $matches[4]);
529
                $constraint_values = trim($constraint_bits[2], ')');
530
                $constraint_values_bits = explode(',', $constraint_values);
531
                $default = trim($constraint_values_bits[0], " '");
532
533
                //Now go and convert anything that's not in this list to 'Page'
534
                //We have to run this as a query, not as part of the alteration queries due to the way they are constructed.
535
                $updateConstraint = '';
536
                $updateConstraint .= "UPDATE \"{$tableName}\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
537
                if ($this->hasTable("{$tableName}_Live")) {
538
                    $updateConstraint .= "UPDATE \"{$tableName}_Live\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
539
                }
540
                if ($this->hasTable("{$tableName}_versions")) {
541
                    $updateConstraint .= "UPDATE \"{$tableName}_versions\" SET \"$colName\"='$default' WHERE \"$colName\" NOT IN ($constraint_values);";
542
                }
543
544
                $this->query($updateConstraint);
545
            }
546
547
            //First, delete any existing constraint on this column, even if it's no longer an enum
548
            if ($constraintExists) {
549
                $alterCol .= ",\nDROP CONSTRAINT \"{$constraintName}\"";
550
            }
551
552
            //Now create the constraint (if we've asked for one)
553
            if (!empty($matches[4])) {
554
                $alterCol .= ",\nADD CONSTRAINT \"{$constraintName}\" $matches[4]";
555
            }
556
        }
557
558
        return isset($alterCol) ? $alterCol : '';
559
    }
560
561
    public function renameTable($oldTableName, $newTableName)
562
    {
563
        $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\"");
564
        unset(self::$cached_fieldlists[$oldTableName]);
565
    }
566
567
    public function checkAndRepairTable($tableName)
568
    {
569
        $this->query("VACUUM FULL ANALYZE \"$tableName\"");
570
        $this->query("REINDEX TABLE \"$tableName\"");
571
        return true;
572
    }
573
574
    public function createField($table, $field, $spec)
575
    {
576
        $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec");
577
    }
578
579
    /**
580
     * Change the database type of the given field.
581
     *
582
     * @param string $tableName The name of the tbale the field is in.
583
     * @param string $fieldName The name of the field to change.
584
     * @param string $fieldSpec The new field specification
585
     */
586
    public function alterField($tableName, $fieldName, $fieldSpec)
587
    {
588
        $this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
589
    }
590
591
    public function renameField($tableName, $oldName, $newName)
592
    {
593
        $fieldList = $this->fieldList($tableName);
594
        if (array_key_exists($oldName, $fieldList)) {
595
            $this->query("ALTER TABLE \"$tableName\" RENAME COLUMN \"$oldName\" TO \"$newName\"");
596
597
            //Remove this from the cached list:
598
            unset(self::$cached_fieldlists[$tableName]);
599
        }
600
    }
601
602
    public function fieldList($table)
603
    {
604
        //Query from http://www.alberton.info/postgresql_meta_info.html
605
        //This gets us more information than we need, but I've included it all for the moment....
606
607
        //if(!isset(self::$cached_fieldlists[$table])){
0 ignored issues
show
Unused Code Comprehensibility introduced by
100% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
608
        $fields = $this->preparedQuery(
609
            "
610
                SELECT ordinal_position, column_name, data_type, column_default,
611
                is_nullable, character_maximum_length, numeric_precision, numeric_scale
612
                FROM information_schema.columns WHERE table_name = ? and table_schema = ?
613
                ORDER BY ordinal_position;",
614
            array($table, $this->database->currentSchema())
615
        );
616
617
        $output = array();
618
        if ($fields) {
619
            foreach ($fields as $field) {
620
                switch ($field['data_type']) {
621
                    case 'character varying':
622
                        //Check to see if there's a constraint attached to this column:
623
                        //$constraint=$this->query("SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE r.contype = 'c' AND conname='" . $table . '_' . $field['column_name'] . "_check' ORDER BY 1;")->first();
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
624
                        $constraint = $this->constraintExists($table . '_' . $field['column_name'] . '_check');
625
                        if ($constraint) {
626
                            //Now we need to break this constraint text into bits so we can see what we have:
627
                            //Examples:
628
                            //CHECK ("CanEditType"::text = ANY (ARRAY['LoggedInUsers'::character varying, 'OnlyTheseUsers'::character varying, 'Inherit'::character varying]::text[]))
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
629
                            //CHECK ("ClassName"::text = 'PageComment'::text)
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
630
631
                            //TODO: replace all this with a regular expression!
632
                            $value = $constraint['pg_get_constraintdef'];
633
                            $value = substr($value, strpos($value, '='));
634
                            $value = str_replace("''", "'", $value);
635
636
                            $in_value = false;
637
                            $constraints = array();
638
                            $current_value = '';
639
                            for ($i = 0; $i < strlen($value); $i++) {
640
                                $char = substr($value, $i, 1);
641
                                if ($in_value) {
642
                                    $current_value .= $char;
643
                                }
644
645
                                if ($char == "'") {
646
                                    if (!$in_value) {
647
                                        $in_value = true;
648
                                    } else {
649
                                        $in_value = false;
650
                                        $constraints[] = substr($current_value, 0, -1);
651
                                        $current_value = '';
652
                                    }
653
                                }
654
                            }
655
656
                            if (sizeof($constraints) > 0) {
657
                                //Get the default:
658
                                $default = trim(substr(
659
                                    $field['column_default'],
660
                                    0,
661
                                    strpos($field['column_default'], '::')
662
                                ), "'");
663
                                $output[$field['column_name']] = $this->enum(array(
664
                                    'default' => $default,
665
                                    'name' => $field['column_name'],
666
                                    'enums' => $constraints
667
                                ));
668
                            }
669
                        } else {
670
                            $output[$field['column_name']] = 'varchar(' . $field['character_maximum_length'] . ')';
671
                        }
672
                        break;
673
674
                    case 'numeric':
675
                        $output[$field['column_name']] = 'decimal(' . $field['numeric_precision'] . ',' . $field['numeric_scale'] . ') default ' . floatval($field['column_default']);
676
                        break;
677
678
                    case 'integer':
679
                        $output[$field['column_name']] = 'integer default ' . (int)$field['column_default'];
680
                        break;
681
682
                    case 'timestamp without time zone':
683
                        $output[$field['column_name']] = 'timestamp';
684
                        break;
685
686
                    case 'smallint':
687
                        $output[$field['column_name']] = 'smallint default ' . (int)$field['column_default'];
688
                        break;
689
690
                    case 'time without time zone':
691
                        $output[$field['column_name']] = 'time';
692
                        break;
693
694
                    case 'double precision':
695
                        $output[$field['column_name']] = 'float';
696
                        break;
697
698
                    default:
699
                        $output[$field['column_name']] = $field;
700
                }
701
            }
702
        }
703
704
        //    self::$cached_fieldlists[$table]=$output;
0 ignored issues
show
Unused Code Comprehensibility introduced by
80% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
705
        //}
706
707
        //return self::$cached_fieldlists[$table];
0 ignored issues
show
Unused Code Comprehensibility introduced by
89% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
708
709
        return $output;
710
    }
711
712
    public function clearCachedFieldlist($tableName = false)
713
    {
714
        if ($tableName) {
715
            unset(self::$cached_fieldlists[$tableName]);
716
        } else {
717
            self::$cached_fieldlists = array();
718
        }
719
        return true;
720
    }
721
722
    /**
723
     * Create an index on a table.
724
     *
725
     * @param string $tableName The name of the table.
726
     * @param string $indexName The name of the index.
727
     * @param string $indexSpec The specification of the index, see Database::requireIndex() for more details.
728
     */
729
    public function createIndex($tableName, $indexName, $indexSpec)
730
    {
731
        $createIndex = $this->getIndexSqlDefinition($tableName, $indexName, $indexSpec);
732
        if ($createIndex !== false) {
733
            $this->query($createIndex);
734
        }
735
    }
736
737
    protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec)
738
    {
739
740
        //TODO: create table partition support
741
        //TODO: create clustering options
742
743
        //NOTE: it is possible for *_renamed tables to have indexes whose names are not updates
744
        //Therefore, we now check for the existance of indexes before we create them.
745
        //This is techically a bug, since new tables will not be indexed.
746
747
        // Determine index name
748
        $tableCol = $this->buildPostgresIndexName($tableName, $indexName);
749
750
        //Misc options first:
751
        $fillfactor = $where = '';
752
        if (isset($indexSpec['fillfactor'])) {
753
            $fillfactor = 'WITH (FILLFACTOR = ' . $indexSpec['fillfactor'] . ')';
754
        }
755
        if (isset($indexSpec['where'])) {
756
            $where = 'WHERE ' . $indexSpec['where'];
757
        }
758
759
        //create a type-specific index
760
        // NOTE:  hash should be removed.  This is only here to demonstrate how other indexes can be made
761
        // NOTE: Quote the index name to preserve case sensitivity
762
        switch ($indexSpec['type']) {
763
            case 'fulltext':
764
                // @see fulltext() for the definition of the trigger that ts_$IndexName uses for fulltext searching
765
                $clusterMethod = PostgreSQLDatabase::default_fts_cluster_method();
766
                $spec = "create index \"$tableCol\" ON \"$tableName\" USING $clusterMethod(\"ts_" . $indexName . "\") $fillfactor $where";
767
                break;
768
769 View Code Duplication
            case 'unique':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
770
                $spec = "create unique index \"$tableCol\" ON \"$tableName\" (" . $this->implodeColumnList($indexSpec['columns']) . ") $fillfactor $where";
771
                break;
772
773 View Code Duplication
            case 'btree':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
774
                $spec = "create index \"$tableCol\" ON \"$tableName\" USING btree (" . $this->implodeColumnList($indexSpec['columns']) . ") $fillfactor $where";
775
                break;
776
777 View Code Duplication
            case 'hash':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
778
                //NOTE: this is not a recommended index type
779
                $spec = "create index \"$tableCol\" ON \"$tableName\" USING hash (" . $this->implodeColumnList($indexSpec['columns']) . ") $fillfactor $where";
780
                break;
781
782
            case 'index':
783
                //'index' is the same as default, just a normal index with the default type decided by the database.
784 View Code Duplication
            default:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
785
                $spec = "create index \"$tableCol\" ON \"$tableName\" (" . $this->implodeColumnList($indexSpec['columns']) . ") $fillfactor $where";
786
        }
787
        return trim($spec) . ';';
788
    }
789
790
    public function alterIndex($tableName, $indexName, $indexSpec)
791
    {
792
        $indexSpec = trim($indexSpec);
793
        if ($indexSpec[0] != '(') {
794
            list($indexType, $indexFields) = explode(' ', $indexSpec, 2);
795
        } else {
796
            $indexType = null;
797
            $indexFields = $indexSpec;
798
        }
799
800
        if (!$indexType) {
801
            $indexType = "index";
802
        }
803
804
        $this->query("DROP INDEX \"$indexName\"");
805
        $this->query("ALTER TABLE \"$tableName\" ADD $indexType \"$indexName\" $indexFields");
806
    }
807
808
    /**
809
     * Given a trigger name attempt to determine the columns upon which it acts
810
     *
811
     * @param string $triggerName Postgres trigger name
812
     * @param string $table
813
     * @return array List of columns
814
     */
815
    protected function extractTriggerColumns($triggerName, $table)
816
    {
817
        $trigger = $this->preparedQuery(
818
            "SELECT t.tgargs 
819
            FROM pg_catalog.pg_trigger t
820
            INNER JOIN pg_catalog.pg_class c ON c.oid = t.tgrelid 
821
            INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
822
            WHERE c.relname = ?
823
                AND n.nspname = ?
824
                AND t.tgname = ?",
825
            [
826
                $table,
827
                $this->database->currentSchema(),
828
                $triggerName
829
            ]
830
        )->first();
831
832
        // Convert stream to string
833
        if (is_resource($trigger['tgargs'])) {
834
            $trigger['tgargs'] = stream_get_contents($trigger['tgargs']);
835
        }
836
837
        if (strpos($trigger['tgargs'], "\000") !== false) {
838
            // Option 1: output as a string (PDO)
839
            $argList = array_filter(explode("\000", $trigger['tgargs']));
840
841
        } else {
842
            // Option 2: hex-encoded (pg_sql non-pdo)
843
            $bytes = str_split($trigger['tgargs'], 2);
844
            $argList = array();
845
            $nextArg = "";
846
            foreach ($bytes as $byte) {
847
                if ($byte == "00") {
848
                    $argList[] = $nextArg;
849
                    $nextArg = "";
850
                } else {
851
                    $nextArg .= chr(hexdec($byte));
852
                }
853
            }
854
        }
855
856
        // Drop first two arguments (trigger name and config name) and implode into nice list
857
        return array_slice($argList, 2);
858
    }
859
860
    public function indexList($table)
861
    {
862
        //Retrieve a list of indexes for the specified table
863
        $indexes = $this->preparedQuery(
864
            "
865
            SELECT tablename, indexname, indexdef
866
            FROM pg_catalog.pg_indexes
867
            WHERE tablename = ? AND schemaname = ?;",
868
            array($table, $this->database->currentSchema())
869
        );
870
871
        $indexList = array();
872
        foreach ($indexes as $index) {
873
            // Key for the indexList array.  Differs from other DB implementations, which is why
874
            // requireIndex() needed to be overridden
875
            $indexName = $index['indexname'];
876
877
            //We don't actually need the entire created command, just a few bits:
878
            $type = '';
879
880
            //Check for uniques:
881
            if (substr($index['indexdef'], 0, 13) == 'CREATE UNIQUE') {
882
                $type = 'unique';
883
            }
884
885
            //check for hashes, btrees etc:
886
            if (strpos(strtolower($index['indexdef']), 'using hash ') !== false) {
887
                $type = 'hash';
888
            }
889
890
            //TODO: Fix me: btree is the default index type:
891
            //if(strpos(strtolower($index['indexdef']), 'using btree ')!==false)
0 ignored issues
show
Unused Code Comprehensibility introduced by
79% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
892
            //    $prefix='using btree ';
893
894
            if (strpos(strtolower($index['indexdef']), 'using rtree ') !== false) {
895
                $type = 'rtree';
896
            }
897
898
            // For fulltext indexes we need to extract the columns from another source
899
            if (stristr($index['indexdef'], 'using gin')) {
900
                $type = 'fulltext';
901
                // Extract trigger information from postgres
902
                $triggerName = preg_replace('/^ix_/', 'ts_', $index['indexname']);
903
                $columns = $this->extractTriggerColumns($triggerName, $table);
904
                $columnString = $this->implodeColumnList($columns);
905
            } else {
906
                $columnString = $this->quoteColumnSpecString($index['indexdef']);
907
            }
908
909
            $indexList[$indexName] = array(
910
                'name' => $indexName, // Not the correct name in the PHP, as this will be a mangled postgres-unique code
911
                'columns' => $this->explodeColumnString($columnString),
912
                'type' => $type ?: 'index',
913
            );
914
        }
915
916
        return $indexList;
917
    }
918
919
    public function tableList()
920
    {
921
        $tables = array();
922
        $result = $this->preparedQuery(
923
            "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename NOT ILIKE 'pg\\\_%' AND tablename NOT ILIKE 'sql\\\_%'",
924
            array($this->database->currentSchema())
925
        );
926
        foreach ($result as $record) {
927
            $table = reset($record);
928
            $tables[strtolower($table)] = $table;
929
        }
930
        return $tables;
931
    }
932
933
    /**
934
     * Find out what the constraint information is, given a constraint name.
935
     * We also cache this result, so the next time we don't need to do a
936
     * query all over again.
937
     *
938
     * @param string $constraint
939
     * @param bool $cache Flag whether a cached version should be used. Set to false to cache bust.
940
     * @return false|array Either false, if the constraint doesn't exist, or an array
941
     * with the keys conname and pg_get_constraintdef
942
     */
943
    protected function constraintExists($constraint, $cache = true)
944
    {
945
        if (!$cache || !isset(self::$cached_constraints[$constraint])) {
946
            $value = $this->preparedQuery(
947
                "
948
                SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true)
949
                FROM pg_catalog.pg_constraint r
950
                INNER JOIN pg_catalog.pg_namespace n
951
                  ON r.connamespace = n.oid
952
                WHERE r.contype = 'c' AND conname = ? AND n.nspname = ?
953
                ORDER BY 1;",
954
                array($constraint, $this->database->currentSchema())
955
            )->first();
956
            if (!$cache) {
957
                return $value;
958
            }
959
            self::$cached_constraints[$constraint] = $value;
960
        }
961
962
        return self::$cached_constraints[$constraint];
963
    }
964
965
    /**
966
     * A function to return the field names and datatypes for the particular table
967
     *
968
     * @param string $tableName
969
     * @return array List of columns an an associative array with the keys Column and DataType
970
     */
971
    public function tableDetails($tableName)
972
    {
973
        $query = "SELECT a.attname as \"Column\", pg_catalog.format_type(a.atttypid, a.atttypmod) as \"Datatype\"
974
                FROM pg_catalog.pg_attribute a
975
                WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attrelid = (
976
                    SELECT c.oid
977
                    FROM pg_catalog.pg_class c
978
                    LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
979
                    WHERE c.relname = ? AND pg_catalog.pg_table_is_visible(c.oid) AND n.nspname = ?
980
                );";
981
982
        $result = $this->preparedQuery(
983
            $query,
984
            array($tableName, $this->database->currentSchema())
985
        );
986
987
        $table = array();
988
        foreach ($result as $row) {
989
            $table[] = array(
990
                'Column' => $row['Column'],
991
                'DataType' => $row['DataType']
992
            );
993
        }
994
995
        return $table;
996
    }
997
998
    /**
999
     * Pass a legit trigger name and it will be dropped
1000
     * This assumes that the trigger has been named in a unique fashion
1001
     *
1002
     * @param string $triggerName Name of the trigger
1003
     * @param string $tableName Name of the table
1004
     */
1005
    protected function dropTrigger($triggerName, $tableName)
1006
    {
1007
        $exists = $this->preparedQuery(
1008
            "
1009
            SELECT trigger_name
1010
            FROM information_schema.triggers
1011
            WHERE trigger_name = ? AND trigger_schema = ?;",
1012
            array($triggerName, $this->database->currentSchema())
1013
        )->first();
1014
        if ($exists) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $exists of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1015
            $this->query("DROP trigger IF EXISTS $triggerName ON \"$tableName\";");
1016
        }
1017
    }
1018
1019
    /**
1020
     * This will return the fields that the trigger is monitoring
1021
     *
1022
     * @param string $trigger Name of the trigger
1023
     * @return array
1024
     */
1025
    protected function triggerFieldsFromTrigger($trigger)
1026
    {
1027
        if ($trigger) {
1028
            $tsvector = 'tsvector_update_trigger';
1029
            $ts_pos = strpos($trigger, $tsvector);
1030
            $details = trim(substr($trigger, $ts_pos + strlen($tsvector)), '();');
1031
            //Now split this into bits:
1032
            $bits = explode(',', $details);
1033
1034
            $fields = $bits[2];
1035
1036
            $field_bits = explode(',', str_replace('"', '', $fields));
1037
            $result = array();
1038
            foreach ($field_bits as $field_bit) {
1039
                $result[] = trim($field_bit);
1040
            }
1041
1042
            return $result;
1043
        } else {
1044
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
1045
        }
1046
    }
1047
1048
    /**
1049
     * Return a boolean type-formatted string
1050
     *
1051
     * @param array $values Contains a tokenised list of info about this data type
1052
     * @return string
1053
     */
1054
    public function boolean($values)
1055
    {
1056
        $default = $values['default'] ? '1' : '0';
1057
        return "smallint default {$default}";
1058
    }
1059
1060
    /**
1061
     * Return a date type-formatted string
1062
     *
1063
     * @param array $values Contains a tokenised list of info about this data type
1064
     * @return string
1065
     */
1066
    public function date($values)
1067
    {
1068
        return "date";
1069
    }
1070
1071
    /**
1072
     * Return a decimal type-formatted string
1073
     *
1074
     * @param array $values Contains a tokenised list of info about this data type
1075
     * @return string
1076
     */
1077
    public function decimal($values)
1078
    {
1079
        // Avoid empty strings being put in the db
1080
        if ($values['precision'] == '') {
1081
            $precision = 1;
1082
        } else {
1083
            $precision = $values['precision'];
1084
        }
1085
1086
        $defaultValue = '';
1087
        if (isset($values['default']) && is_numeric($values['default'])) {
1088
            $defaultValue = ' default ' . floatval($values['default']);
1089
        }
1090
1091
        return "decimal($precision)$defaultValue";
1092
    }
1093
1094
    /**
1095
     * Return a enum type-formatted string
1096
     *
1097
     * @param array $values Contains a tokenised list of info about this data type
1098
     * @return string
1099
     */
1100
    public function enum($values)
1101
    {
1102
        $default = " default '{$values['default']}'";
1103
        return "varchar(255)" . $default . " check (\"" . $values['name'] . "\" in ('" . implode(
1104
            '\', \'',
1105
            $values['enums']
1106
        ) . "', null))";
1107
    }
1108
1109
    /**
1110
     * Return a float type-formatted string
1111
     *
1112
     * @param array $values Contains a tokenised list of info about this data type
1113
     * @return string
1114
     */
1115
    public function float($values)
1116
    {
1117
        return "float";
1118
    }
1119
1120
    /**
1121
     * Return a float type-formatted string cause double is not supported
1122
     *
1123
     * @param array $values Contains a tokenised list of info about this data type
1124
     * @return string
1125
     */
1126
    public function double($values)
1127
    {
1128
        return $this->float($values);
1129
    }
1130
1131
    /**
1132
     * Return a int type-formatted string
1133
     *
1134
     * @param array $values Contains a tokenised list of info about this data type
1135
     * @return string
1136
     */
1137
    public function int($values)
1138
    {
1139
        return "integer default " . (int)$values['default'];
1140
    }
1141
1142
    /**
1143
     * Return a bigint type-formatted string
1144
     *
1145
     * @param array $values Contains a tokenised list of info about this data type
1146
     * @return string
1147
     */
1148
    public function bigint($values)
1149
    {
1150
        return "bigint default " . (int)$values['default'];
1151
    }
1152
1153
    /**
1154
     * Return a datetime type-formatted string
1155
     * For PostgreSQL, we simply return the word 'timestamp', no other parameters are necessary
1156
     *
1157
     * @param array $values Contains a tokenised list of info about this data type
1158
     * @return string
1159
     */
1160
    public function datetime($values)
1161
    {
1162
        return "timestamp";
1163
    }
1164
1165
    /**
1166
     * Return a text type-formatted string
1167
     *
1168
     * @param array $values Contains a tokenised list of info about this data type
1169
     * @return string
1170
     */
1171
    public function text($values)
1172
    {
1173
        return "text";
1174
    }
1175
1176
    /**
1177
     * Return a time type-formatted string
1178
     *
1179
     * @param array $values Contains a tokenised list of info about this data type
1180
     * @return string
1181
     */
1182
    public function time($values)
1183
    {
1184
        return "time";
1185
    }
1186
1187
    /**
1188
     * Return a varchar type-formatted string
1189
     *
1190
     * @param array $values Contains a tokenised list of info about this data type
1191
     * @return string
1192
     */
1193
    public function varchar($values)
1194
    {
1195
        if (!isset($values['precision'])) {
1196
            $values['precision'] = 255;
1197
        }
1198
1199
        return "varchar({$values['precision']})";
1200
    }
1201
1202
    /*
1203
     * Return a 4 digit numeric type.  MySQL has a proprietary 'Year' type.
1204
     * For Postgres, we'll use a 4 digit numeric
1205
     *
1206
     * @param array $values Contains a tokenised list of info about this data type
1207
     * @return string
1208
     */
1209
    public function year($values)
1210
    {
1211
        return "decimal(4,0)";
1212
    }
1213
1214
    /**
1215
     * Create a fulltext search datatype for PostgreSQL
1216
     * This will also return a trigger to be applied to this table
1217
     *
1218
     * @todo: create custom functions to allow weighted searches
1219
     *
1220
     * @param array $this_index Index specification for the fulltext index
1221
     * @param string $tableName
1222
     * @param string $name
1223
     * @return array
1224
     */
1225
    protected function fulltext($this_index, $tableName, $name)
1226
    {
1227
        //For full text search, we need to create a column for the index
1228
        $columns = $this->implodeColumnList($this_index['columns']);
1229
1230
        $fulltexts = "\"ts_$name\" tsvector";
1231
        $triggerName = $this->buildPostgresTriggerName($tableName, $name);
1232
        $language = PostgreSQLDatabase::search_language();
1233
1234
        $this->dropTrigger($triggerName, $tableName);
1235
        $triggers = "CREATE TRIGGER \"$triggerName\" BEFORE INSERT OR UPDATE
1236
                    ON \"$tableName\" FOR EACH ROW EXECUTE PROCEDURE
1237
                    tsvector_update_trigger(\"ts_$name\", 'pg_catalog.$language', $columns);";
1238
1239
        return array(
1240
            'name' => $name,
1241
            'ts_name' => "ts_{$name}",
1242
            'fulltexts' => $fulltexts,
1243
            'triggers' => $triggers
1244
        );
1245
    }
1246
1247
    public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
1248
    {
1249
        if ($asDbValue) {
1250
            return 'bigint';
1251
        } else {
1252
            return 'serial8 not null';
1253
        }
1254
    }
1255
1256
    public function hasTable($tableName)
1257
    {
1258
        $result = $this->preparedQuery(
1259
            "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename = ?;",
1260
            array($this->database->currentSchema(), $tableName)
1261
        );
1262
        return ($result->numRecords() > 0);
1263
    }
1264
1265
    /**
1266
     * Returns the values of the given enum field
1267
     *
1268
     * @todo Make a proper implementation
1269
     *
1270
     * @param string $tableName Name of table to check
1271
     * @param string $fieldName name of enum field to check
1272
     * @return array List of enum values
1273
     */
1274
    public function enumValuesForField($tableName, $fieldName)
1275
    {
1276
        //return array('SiteTree','Page');
0 ignored issues
show
Unused Code Comprehensibility introduced by
89% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1277
        $constraints = $this->constraintExists("{$tableName}_{$fieldName}_check");
1278
        if ($constraints) {
1279
            return $this->enumValuesFromConstraint($constraints['pg_get_constraintdef']);
1280
        } else {
1281
            return array();
1282
        }
1283
    }
1284
1285
    /**
1286
     * Get the actual enum fields from the constraint value:
1287
     *
1288
     * @param string $constraint
1289
     * @return array
1290
     */
1291
    protected function enumValuesFromConstraint($constraint)
1292
    {
1293
        $constraint = substr($constraint, strpos($constraint, 'ANY (ARRAY[') + 11);
1294
        $constraint = substr($constraint, 0, -11);
1295
        $constraints = array();
1296
        $segments = explode(',', $constraint);
1297
        foreach ($segments as $this_segment) {
1298
            $bits = preg_split('/ *:: */', $this_segment);
1299
            array_unshift($constraints, trim($bits[0], " '"));
1300
        }
1301
        return $constraints;
1302
    }
1303
1304
    public function dbDataType($type)
1305
    {
1306
        $values = array(
1307
            'unsigned integer' => 'INT'
1308
        );
1309
1310
        if (isset($values[$type])) {
1311
            return $values[$type];
1312
        } else {
1313
            return '';
1314
        }
1315
    }
1316
1317
    /*
1318
     * Given a tablespace and and location, either create a new one
1319
     * or update the existing one
1320
     *
1321
     * @param string $name
1322
     * @param string $location
1323
     */
1324
    public function createOrReplaceTablespace($name, $location)
1325
    {
1326
        $existing = $this->preparedQuery(
1327
            "SELECT spcname, spclocation FROM pg_tablespace WHERE spcname = ?;",
1328
            array($name)
1329
        )->first();
1330
1331
        //NOTE: this location must be empty for this to work
1332
        //We can't seem to change the location of the tablespace through any ALTER commands :(
1333
1334
        //If a tablespace with this name exists, but the location has changed, then drop the current one
1335
        //if($existing && $location!=$existing['spclocation'])
0 ignored issues
show
Unused Code Comprehensibility introduced by
79% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1336
        //    DB::query("DROP TABLESPACE $name;");
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1337
1338
        //If this is a new tablespace, or we have dropped the current one:
1339
        if (!$existing || ($existing && $location != $existing['spclocation'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $existing of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug Best Practice introduced by
The expression $existing of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1340
            $this->query("CREATE TABLESPACE $name LOCATION '$location';");
1341
        }
1342
    }
1343
1344
    /**
1345
     *
1346
     * @param string $tableName
1347
     * @param array $partitions
1348
     * @param array $indexes
1349
     * @param array $extensions
1350
     */
1351
    public function createOrReplacePartition($tableName, $partitions, $indexes = [], $extensions = [])
1352
    {
1353
1354
        //We need the plpgsql language to be installed for this to work:
1355
        $this->createLanguage('plpgsql');
1356
1357
        $trigger = 'CREATE OR REPLACE FUNCTION ' . $tableName . '_insert_trigger() RETURNS TRIGGER AS $$ BEGIN ';
1358
        $first = true;
1359
1360
        //Do we need to create a tablespace for this item?
1361 View Code Duplication
        if ($extensions && isset($extensions['tablespace'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1362
            $this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']);
1363
            $tableSpace = ' TABLESPACE ' . $extensions['tablespace']['name'];
1364
        } else {
1365
            $tableSpace = '';
1366
        }
1367
1368
        foreach ($partitions as $partition_name => $partition_value) {
1369
            //Check that this child table does not already exist:
1370
            if (!$this->hasTable($partition_name)) {
1371
                $this->query("CREATE TABLE \"$partition_name\" (CHECK (" . str_replace(
1372
                    'NEW.',
1373
                    '',
1374
                    $partition_value
1375
                ) . ")) INHERITS (\"$tableName\")$tableSpace;");
1376
            } else {
1377
                //Drop the constraint, we will recreate in in the next line
1378
                $constraintName = "{$partition_name}_pkey";
1379
                $constraintExists = $this->constraintExists($constraintName, false);
1380
                if ($constraintExists) {
1381
                    $this->query("ALTER TABLE \"$partition_name\" DROP CONSTRAINT \"{$constraintName}\";");
1382
                }
1383
                $this->dropTrigger(strtolower('trigger_' . $tableName . '_insert'), $tableName);
1384
            }
1385
1386
            $this->query("ALTER TABLE \"$partition_name\" ADD CONSTRAINT \"{$partition_name}_pkey\" PRIMARY KEY (\"ID\");");
1387
1388
            if ($first) {
1389
                $trigger .= 'IF';
1390
                $first = false;
1391
            } else {
1392
                $trigger .= 'ELSIF';
1393
            }
1394
1395
            $trigger .= " ($partition_value) THEN INSERT INTO \"$partition_name\" VALUES (NEW.*);";
1396
1397
            if ($indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1398
                // We need to propogate the indexes through to the child pages.
1399
                // Some of this code is duplicated, and could be tidied up
1400
                foreach ($indexes as $name => $this_index) {
1401
                    if ($this_index['type'] == 'fulltext') {
1402
                        $fillfactor = $where = '';
1403
                        if (isset($this_index['fillfactor'])) {
1404
                            $fillfactor = 'WITH (FILLFACTOR = ' . $this_index['fillfactor'] . ')';
1405
                        }
1406
                        if (isset($this_index['where'])) {
1407
                            $where = 'WHERE ' . $this_index['where'];
1408
                        }
1409
                        $clusterMethod = PostgreSQLDatabase::default_fts_cluster_method();
1410
                        $this->query("CREATE INDEX \"" . $this->buildPostgresIndexName(
1411
                            $partition_name,
1412
                            $this_index['name']
1413
                        ) . "\" ON \"" . $partition_name . "\" USING $clusterMethod(\"ts_" . $name . "\") $fillfactor $where");
1414
                        $ts_details = $this->fulltext($this_index, $partition_name, $name);
1415
                        $this->query($ts_details['triggers']);
1416
                    } else {
1417
                        if (is_array($this_index)) {
1418
                            $index_name = $this_index['name'];
1419
                        } else {
1420
                            $index_name = trim($this_index, '()');
1421
                        }
1422
1423
                        $createIndex = $this->getIndexSqlDefinition($partition_name, $index_name, $this_index);
1424
                        if ($createIndex !== false) {
1425
                            $this->query($createIndex);
1426
                        }
1427
                    }
1428
                }
1429
            }
1430
1431
            //Lastly, clustering goes here:
1432
            if ($extensions && isset($extensions['cluster'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1433
                $this->query("CLUSTER \"$partition_name\" USING \"{$extensions['cluster']}\";");
1434
            }
1435
        }
1436
1437
        $trigger .= 'ELSE RAISE EXCEPTION \'Value id out of range.  Fix the ' . $tableName . '_insert_trigger() function!\'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql;';
1438
        $trigger .= 'CREATE TRIGGER trigger_' . $tableName . '_insert BEFORE INSERT ON "' . $tableName . '" FOR EACH ROW EXECUTE PROCEDURE ' . $tableName . '_insert_trigger();';
1439
1440
        $this->query($trigger);
1441
    }
1442
1443
    /*
1444
     * This will create a language if it doesn't already exist.
1445
     * This is used by the createOrReplacePartition function, which needs plpgsql
1446
     *
1447
     * @param string $language Language name
1448
     */
1449
    public function createLanguage($language)
1450
    {
1451
        $result = $this->preparedQuery(
1452
            "SELECT lanname FROM pg_language WHERE lanname = ?;",
1453
            array($language)
1454
        )->first();
1455
1456
        if (!$result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1457
            $this->query("CREATE LANGUAGE $language;");
1458
        }
1459
    }
1460
1461
    /**
1462
     * Return a set type-formatted string
1463
     * This is used for Multi-enum support, which isn't actually supported by Postgres.
1464
     * Throws a user error to show our lack of support, and return an "int", specifically for sapphire
1465
     * tests that test multi-enums. This results in a test failure, but not crashing the test run.
1466
     *
1467
     * @param array $values Contains a tokenised list of info about this data type
1468
     * @return string
1469
     */
1470
    public function set($values)
1471
    {
1472
        user_error("PostGreSQL does not support multi-enum", E_USER_ERROR);
1473
        return "int";
1474
    }
1475
}
1476