Completed
Push — master ( 8c5f95...e41df6 )
by
unknown
11s
created

PostgreSQLSchemaManager::set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
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
        } else {
841
            // Option 2: hex-encoded (pg_sql non-pdo)
842
            $bytes = str_split($trigger['tgargs'], 2);
843
            $argList = array();
844
            $nextArg = "";
845
            foreach ($bytes as $byte) {
846
                if ($byte == "00") {
847
                    $argList[] = $nextArg;
848
                    $nextArg = "";
849
                } else {
850
                    $nextArg .= chr(hexdec($byte));
851
                }
852
            }
853
        }
854
855
        // Drop first two arguments (trigger name and config name) and implode into nice list
856
        return array_slice($argList, 2);
857
    }
858
859
    public function indexList($table)
860
    {
861
        //Retrieve a list of indexes for the specified table
862
        $indexes = $this->preparedQuery(
863
            "
864
            SELECT tablename, indexname, indexdef
865
            FROM pg_catalog.pg_indexes
866
            WHERE tablename = ? AND schemaname = ?;",
867
            array($table, $this->database->currentSchema())
868
        );
869
870
        $indexList = array();
871
        foreach ($indexes as $index) {
872
            // Key for the indexList array.  Differs from other DB implementations, which is why
873
            // requireIndex() needed to be overridden
874
            $indexName = $index['indexname'];
875
876
            //We don't actually need the entire created command, just a few bits:
877
            $type = '';
878
879
            //Check for uniques:
880
            if (substr($index['indexdef'], 0, 13) == 'CREATE UNIQUE') {
881
                $type = 'unique';
882
            }
883
884
            //check for hashes, btrees etc:
885
            if (strpos(strtolower($index['indexdef']), 'using hash ') !== false) {
886
                $type = 'hash';
887
            }
888
889
            //TODO: Fix me: btree is the default index type:
890
            //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...
891
            //    $prefix='using btree ';
892
893
            if (strpos(strtolower($index['indexdef']), 'using rtree ') !== false) {
894
                $type = 'rtree';
895
            }
896
897
            // For fulltext indexes we need to extract the columns from another source
898
            if (stristr($index['indexdef'], 'using gin')) {
899
                $type = 'fulltext';
900
                // Extract trigger information from postgres
901
                $triggerName = preg_replace('/^ix_/', 'ts_', $index['indexname']);
902
                $columns = $this->extractTriggerColumns($triggerName, $table);
903
                $columnString = $this->implodeColumnList($columns);
904
            } else {
905
                $columnString = $this->quoteColumnSpecString($index['indexdef']);
906
            }
907
908
            $indexList[$indexName] = array(
909
                'name' => $indexName, // Not the correct name in the PHP, as this will be a mangled postgres-unique code
910
                'columns' => $this->explodeColumnString($columnString),
911
                'type' => $type ?: 'index',
912
            );
913
        }
914
915
        return $indexList;
916
    }
917
918
    public function tableList()
919
    {
920
        $tables = array();
921
        $result = $this->preparedQuery(
922
            "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename NOT ILIKE 'pg\\\_%' AND tablename NOT ILIKE 'sql\\\_%'",
923
            array($this->database->currentSchema())
924
        );
925
        foreach ($result as $record) {
926
            $table = reset($record);
927
            $tables[strtolower($table)] = $table;
928
        }
929
        return $tables;
930
    }
931
932
    /**
933
     * Find out what the constraint information is, given a constraint name.
934
     * We also cache this result, so the next time we don't need to do a
935
     * query all over again.
936
     *
937
     * @param string $constraint
938
     * @param bool $cache Flag whether a cached version should be used. Set to false to cache bust.
939
     * @return false|array Either false, if the constraint doesn't exist, or an array
940
     * with the keys conname and pg_get_constraintdef
941
     */
942
    protected function constraintExists($constraint, $cache = true)
943
    {
944
        if (!$cache || !isset(self::$cached_constraints[$constraint])) {
945
            $value = $this->preparedQuery(
946
                "
947
                SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true)
948
                FROM pg_catalog.pg_constraint r
949
                INNER JOIN pg_catalog.pg_namespace n
950
                  ON r.connamespace = n.oid
951
                WHERE r.contype = 'c' AND conname = ? AND n.nspname = ?
952
                ORDER BY 1;",
953
                array($constraint, $this->database->currentSchema())
954
            )->first();
955
            if (!$cache) {
956
                return $value;
957
            }
958
            self::$cached_constraints[$constraint] = $value;
959
        }
960
961
        return self::$cached_constraints[$constraint];
962
    }
963
964
    /**
965
     * A function to return the field names and datatypes for the particular table
966
     *
967
     * @param string $tableName
968
     * @return array List of columns an an associative array with the keys Column and DataType
969
     */
970
    public function tableDetails($tableName)
971
    {
972
        $query = "SELECT a.attname as \"Column\", pg_catalog.format_type(a.atttypid, a.atttypmod) as \"Datatype\"
973
                FROM pg_catalog.pg_attribute a
974
                WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attrelid = (
975
                    SELECT c.oid
976
                    FROM pg_catalog.pg_class c
977
                    LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
978
                    WHERE c.relname = ? AND pg_catalog.pg_table_is_visible(c.oid) AND n.nspname = ?
979
                );";
980
981
        $result = $this->preparedQuery(
982
            $query,
983
            array($tableName, $this->database->currentSchema())
984
        );
985
986
        $table = array();
987
        foreach ($result as $row) {
988
            $table[] = array(
989
                'Column' => $row['Column'],
990
                'DataType' => $row['DataType']
991
            );
992
        }
993
994
        return $table;
995
    }
996
997
    /**
998
     * Pass a legit trigger name and it will be dropped
999
     * This assumes that the trigger has been named in a unique fashion
1000
     *
1001
     * @param string $triggerName Name of the trigger
1002
     * @param string $tableName Name of the table
1003
     */
1004
    protected function dropTrigger($triggerName, $tableName)
1005
    {
1006
        $exists = $this->preparedQuery(
1007
            "
1008
            SELECT trigger_name
1009
            FROM information_schema.triggers
1010
            WHERE trigger_name = ? AND trigger_schema = ?;",
1011
            array($triggerName, $this->database->currentSchema())
1012
        )->first();
1013
        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...
1014
            $this->query("DROP trigger IF EXISTS $triggerName ON \"$tableName\";");
1015
        }
1016
    }
1017
1018
    /**
1019
     * This will return the fields that the trigger is monitoring
1020
     *
1021
     * @param string $trigger Name of the trigger
1022
     * @return array
1023
     */
1024
    protected function triggerFieldsFromTrigger($trigger)
1025
    {
1026
        if ($trigger) {
1027
            $tsvector = 'tsvector_update_trigger';
1028
            $ts_pos = strpos($trigger, $tsvector);
1029
            $details = trim(substr($trigger, $ts_pos + strlen($tsvector)), '();');
1030
            //Now split this into bits:
1031
            $bits = explode(',', $details);
1032
1033
            $fields = $bits[2];
1034
1035
            $field_bits = explode(',', str_replace('"', '', $fields));
1036
            $result = array();
1037
            foreach ($field_bits as $field_bit) {
1038
                $result[] = trim($field_bit);
1039
            }
1040
1041
            return $result;
1042
        } else {
1043
            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...
1044
        }
1045
    }
1046
1047
    /**
1048
     * Return a boolean type-formatted string
1049
     *
1050
     * @param array $values Contains a tokenised list of info about this data type
1051
     * @return string
1052
     */
1053
    public function boolean($values)
1054
    {
1055
        $default = $values['default'] ? '1' : '0';
1056
        return "smallint default {$default}";
1057
    }
1058
1059
    /**
1060
     * Return a date type-formatted string
1061
     *
1062
     * @param array $values Contains a tokenised list of info about this data type
1063
     * @return string
1064
     */
1065
    public function date($values)
1066
    {
1067
        return "date";
1068
    }
1069
1070
    /**
1071
     * Return a decimal type-formatted string
1072
     *
1073
     * @param array $values Contains a tokenised list of info about this data type
1074
     * @return string
1075
     */
1076
    public function decimal($values)
1077
    {
1078
        // Avoid empty strings being put in the db
1079
        if ($values['precision'] == '') {
1080
            $precision = 1;
1081
        } else {
1082
            $precision = $values['precision'];
1083
        }
1084
1085
        $defaultValue = '';
1086
        if (isset($values['default']) && is_numeric($values['default'])) {
1087
            $defaultValue = ' default ' . floatval($values['default']);
1088
        }
1089
1090
        return "decimal($precision)$defaultValue";
1091
    }
1092
1093
    /**
1094
     * Return a enum type-formatted string
1095
     *
1096
     * @param array $values Contains a tokenised list of info about this data type
1097
     * @return string
1098
     */
1099
    public function enum($values)
1100
    {
1101
        $default = " default '{$values['default']}'";
1102
        return "varchar(255)" . $default . " check (\"" . $values['name'] . "\" in ('" . implode(
1103
            '\', \'',
1104
            $values['enums']
1105
        ) . "', null))";
1106
    }
1107
1108
    /**
1109
     * Return a float type-formatted string
1110
     *
1111
     * @param array $values Contains a tokenised list of info about this data type
1112
     * @return string
1113
     */
1114
    public function float($values)
1115
    {
1116
        return "float";
1117
    }
1118
1119
    /**
1120
     * Return a float type-formatted string cause double is not supported
1121
     *
1122
     * @param array $values Contains a tokenised list of info about this data type
1123
     * @return string
1124
     */
1125
    public function double($values)
1126
    {
1127
        return $this->float($values);
1128
    }
1129
1130
    /**
1131
     * Return a int type-formatted string
1132
     *
1133
     * @param array $values Contains a tokenised list of info about this data type
1134
     * @return string
1135
     */
1136
    public function int($values)
1137
    {
1138
        return "integer default " . (int)$values['default'];
1139
    }
1140
1141
    /**
1142
     * Return a bigint type-formatted string
1143
     *
1144
     * @param array $values Contains a tokenised list of info about this data type
1145
     * @return string
1146
     */
1147
    public function bigint($values)
1148
    {
1149
        return "bigint default " . (int)$values['default'];
1150
    }
1151
1152
    /**
1153
     * Return a datetime type-formatted string
1154
     * For PostgreSQL, we simply return the word 'timestamp', no other parameters are necessary
1155
     *
1156
     * @param array $values Contains a tokenised list of info about this data type
1157
     * @return string
1158
     */
1159
    public function datetime($values)
1160
    {
1161
        return "timestamp";
1162
    }
1163
1164
    /**
1165
     * Return a text type-formatted string
1166
     *
1167
     * @param array $values Contains a tokenised list of info about this data type
1168
     * @return string
1169
     */
1170
    public function text($values)
1171
    {
1172
        return "text";
1173
    }
1174
1175
    /**
1176
     * Return a time type-formatted string
1177
     *
1178
     * @param array $values Contains a tokenised list of info about this data type
1179
     * @return string
1180
     */
1181
    public function time($values)
1182
    {
1183
        return "time";
1184
    }
1185
1186
    /**
1187
     * Return a varchar type-formatted string
1188
     *
1189
     * @param array $values Contains a tokenised list of info about this data type
1190
     * @return string
1191
     */
1192
    public function varchar($values)
1193
    {
1194
        if (!isset($values['precision'])) {
1195
            $values['precision'] = 255;
1196
        }
1197
1198
        return "varchar({$values['precision']})";
1199
    }
1200
1201
    /*
1202
     * Return a 4 digit numeric type.  MySQL has a proprietary 'Year' type.
1203
     * For Postgres, we'll use a 4 digit numeric
1204
     *
1205
     * @param array $values Contains a tokenised list of info about this data type
1206
     * @return string
1207
     */
1208
    public function year($values)
1209
    {
1210
        return "decimal(4,0)";
1211
    }
1212
1213
    /**
1214
     * Create a fulltext search datatype for PostgreSQL
1215
     * This will also return a trigger to be applied to this table
1216
     *
1217
     * @todo: create custom functions to allow weighted searches
1218
     *
1219
     * @param array $this_index Index specification for the fulltext index
1220
     * @param string $tableName
1221
     * @param string $name
1222
     * @return array
1223
     */
1224
    protected function fulltext($this_index, $tableName, $name)
1225
    {
1226
        //For full text search, we need to create a column for the index
1227
        $columns = $this->implodeColumnList($this_index['columns']);
1228
1229
        $fulltexts = "\"ts_$name\" tsvector";
1230
        $triggerName = $this->buildPostgresTriggerName($tableName, $name);
1231
        $language = PostgreSQLDatabase::search_language();
1232
1233
        $this->dropTrigger($triggerName, $tableName);
1234
        $triggers = "CREATE TRIGGER \"$triggerName\" BEFORE INSERT OR UPDATE
1235
                    ON \"$tableName\" FOR EACH ROW EXECUTE PROCEDURE
1236
                    tsvector_update_trigger(\"ts_$name\", 'pg_catalog.$language', $columns);";
1237
1238
        return array(
1239
            'name' => $name,
1240
            'ts_name' => "ts_{$name}",
1241
            'fulltexts' => $fulltexts,
1242
            'triggers' => $triggers
1243
        );
1244
    }
1245
1246
    public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
1247
    {
1248
        if ($asDbValue) {
1249
            return 'bigint';
1250
        } else {
1251
            return 'serial8 not null';
1252
        }
1253
    }
1254
1255
    public function hasTable($tableName)
1256
    {
1257
        $result = $this->preparedQuery(
1258
            "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = ? AND tablename = ?;",
1259
            array($this->database->currentSchema(), $tableName)
1260
        );
1261
        return ($result->numRecords() > 0);
1262
    }
1263
1264
    /**
1265
     * Returns the values of the given enum field
1266
     *
1267
     * @todo Make a proper implementation
1268
     *
1269
     * @param string $tableName Name of table to check
1270
     * @param string $fieldName name of enum field to check
1271
     * @return array List of enum values
1272
     */
1273
    public function enumValuesForField($tableName, $fieldName)
1274
    {
1275
        //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...
1276
        $constraints = $this->constraintExists("{$tableName}_{$fieldName}_check");
1277
        if ($constraints) {
1278
            return $this->enumValuesFromConstraint($constraints['pg_get_constraintdef']);
1279
        } else {
1280
            return array();
1281
        }
1282
    }
1283
1284
    /**
1285
     * Get the actual enum fields from the constraint value:
1286
     *
1287
     * @param string $constraint
1288
     * @return array
1289
     */
1290
    protected function enumValuesFromConstraint($constraint)
1291
    {
1292
        $constraint = substr($constraint, strpos($constraint, 'ANY (ARRAY[') + 11);
1293
        $constraint = substr($constraint, 0, -11);
1294
        $constraints = array();
1295
        $segments = explode(',', $constraint);
1296
        foreach ($segments as $this_segment) {
1297
            $bits = preg_split('/ *:: */', $this_segment);
1298
            array_unshift($constraints, trim($bits[0], " '"));
1299
        }
1300
        return $constraints;
1301
    }
1302
1303
    public function dbDataType($type)
1304
    {
1305
        $values = array(
1306
            'unsigned integer' => 'INT'
1307
        );
1308
1309
        if (isset($values[$type])) {
1310
            return $values[$type];
1311
        } else {
1312
            return '';
1313
        }
1314
    }
1315
1316
    /*
1317
     * Given a tablespace and and location, either create a new one
1318
     * or update the existing one
1319
     *
1320
     * @param string $name
1321
     * @param string $location
1322
     */
1323
    public function createOrReplaceTablespace($name, $location)
1324
    {
1325
        $existing = $this->preparedQuery(
1326
            "SELECT spcname, spclocation FROM pg_tablespace WHERE spcname = ?;",
1327
            array($name)
1328
        )->first();
1329
1330
        //NOTE: this location must be empty for this to work
1331
        //We can't seem to change the location of the tablespace through any ALTER commands :(
1332
1333
        //If a tablespace with this name exists, but the location has changed, then drop the current one
1334
        //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...
1335
        //    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...
1336
1337
        //If this is a new tablespace, or we have dropped the current one:
1338
        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...
1339
            $this->query("CREATE TABLESPACE $name LOCATION '$location';");
1340
        }
1341
    }
1342
1343
    /**
1344
     *
1345
     * @param string $tableName
1346
     * @param array $partitions
1347
     * @param array $indexes
1348
     * @param array $extensions
1349
     */
1350
    public function createOrReplacePartition($tableName, $partitions, $indexes = [], $extensions = [])
1351
    {
1352
1353
        //We need the plpgsql language to be installed for this to work:
1354
        $this->createLanguage('plpgsql');
1355
1356
        $trigger = 'CREATE OR REPLACE FUNCTION ' . $tableName . '_insert_trigger() RETURNS TRIGGER AS $$ BEGIN ';
1357
        $first = true;
1358
1359
        //Do we need to create a tablespace for this item?
1360 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...
1361
            $this->createOrReplaceTablespace($extensions['tablespace']['name'], $extensions['tablespace']['location']);
1362
            $tableSpace = ' TABLESPACE ' . $extensions['tablespace']['name'];
1363
        } else {
1364
            $tableSpace = '';
1365
        }
1366
1367
        foreach ($partitions as $partition_name => $partition_value) {
1368
            //Check that this child table does not already exist:
1369
            if (!$this->hasTable($partition_name)) {
1370
                $this->query("CREATE TABLE \"$partition_name\" (CHECK (" . str_replace(
1371
                    'NEW.',
1372
                    '',
1373
                    $partition_value
1374
                ) . ")) INHERITS (\"$tableName\")$tableSpace;");
1375
            } else {
1376
                //Drop the constraint, we will recreate in in the next line
1377
                $constraintName = "{$partition_name}_pkey";
1378
                $constraintExists = $this->constraintExists($constraintName, false);
1379
                if ($constraintExists) {
1380
                    $this->query("ALTER TABLE \"$partition_name\" DROP CONSTRAINT \"{$constraintName}\";");
1381
                }
1382
                $this->dropTrigger(strtolower('trigger_' . $tableName . '_insert'), $tableName);
1383
            }
1384
1385
            $this->query("ALTER TABLE \"$partition_name\" ADD CONSTRAINT \"{$partition_name}_pkey\" PRIMARY KEY (\"ID\");");
1386
1387
            if ($first) {
1388
                $trigger .= 'IF';
1389
                $first = false;
1390
            } else {
1391
                $trigger .= 'ELSIF';
1392
            }
1393
1394
            $trigger .= " ($partition_value) THEN INSERT INTO \"$partition_name\" VALUES (NEW.*);";
1395
1396
            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...
1397
                // We need to propogate the indexes through to the child pages.
1398
                // Some of this code is duplicated, and could be tidied up
1399
                foreach ($indexes as $name => $this_index) {
1400
                    if ($this_index['type'] == 'fulltext') {
1401
                        $fillfactor = $where = '';
1402
                        if (isset($this_index['fillfactor'])) {
1403
                            $fillfactor = 'WITH (FILLFACTOR = ' . $this_index['fillfactor'] . ')';
1404
                        }
1405
                        if (isset($this_index['where'])) {
1406
                            $where = 'WHERE ' . $this_index['where'];
1407
                        }
1408
                        $clusterMethod = PostgreSQLDatabase::default_fts_cluster_method();
1409
                        $this->query("CREATE INDEX \"" . $this->buildPostgresIndexName(
1410
                            $partition_name,
1411
                            $this_index['name']
1412
                        ) . "\" ON \"" . $partition_name . "\" USING $clusterMethod(\"ts_" . $name . "\") $fillfactor $where");
1413
                        $ts_details = $this->fulltext($this_index, $partition_name, $name);
1414
                        $this->query($ts_details['triggers']);
1415
                    } else {
1416
                        if (is_array($this_index)) {
1417
                            $index_name = $this_index['name'];
1418
                        } else {
1419
                            $index_name = trim($this_index, '()');
1420
                        }
1421
1422
                        $createIndex = $this->getIndexSqlDefinition($partition_name, $index_name, $this_index);
1423
                        if ($createIndex !== false) {
1424
                            $this->query($createIndex);
1425
                        }
1426
                    }
1427
                }
1428
            }
1429
1430
            //Lastly, clustering goes here:
1431
            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...
1432
                $this->query("CLUSTER \"$partition_name\" USING \"{$extensions['cluster']}\";");
1433
            }
1434
        }
1435
1436
        $trigger .= 'ELSE RAISE EXCEPTION \'Value id out of range.  Fix the ' . $tableName . '_insert_trigger() function!\'; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql;';
1437
        $trigger .= 'CREATE TRIGGER trigger_' . $tableName . '_insert BEFORE INSERT ON "' . $tableName . '" FOR EACH ROW EXECUTE PROCEDURE ' . $tableName . '_insert_trigger();';
1438
1439
        $this->query($trigger);
1440
    }
1441
1442
    /*
1443
     * This will create a language if it doesn't already exist.
1444
     * This is used by the createOrReplacePartition function, which needs plpgsql
1445
     *
1446
     * @param string $language Language name
1447
     */
1448
    public function createLanguage($language)
1449
    {
1450
        $result = $this->preparedQuery(
1451
            "SELECT lanname FROM pg_language WHERE lanname = ?;",
1452
            array($language)
1453
        )->first();
1454
1455
        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...
1456
            $this->query("CREATE LANGUAGE $language;");
1457
        }
1458
    }
1459
1460
    /**
1461
     * Return a set type-formatted string
1462
     * This is used for Multi-enum support, which isn't actually supported by Postgres.
1463
     * Throws a user error to show our lack of support, and return an "int", specifically for sapphire
1464
     * tests that test multi-enums. This results in a test failure, but not crashing the test run.
1465
     *
1466
     * @param array $values Contains a tokenised list of info about this data type
1467
     * @return string
1468
     */
1469
    public function set($values)
1470
    {
1471
        user_error("PostGreSQL does not support multi-enum", E_USER_ERROR);
1472
        return "int";
1473
    }
1474
}
1475