Test Failed
Pull Request — master (#2850)
by Adrien
60:48
created

PostgreSqlSchemaManager::fixDefaultValueQuotes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 6
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\DBAL\Schema;
21
22
use Doctrine\DBAL\Exception\DriverException;
23
use Doctrine\DBAL\Types\Type;
24
25
/**
26
 * PostgreSQL Schema Manager.
27
 *
28
 * @author Konsta Vesterinen <[email protected]>
29
 * @author Lukas Smith <[email protected]> (PEAR MDB2 library)
30
 * @author Benjamin Eberlei <[email protected]>
31
 * @since  2.0
32
 */
33
class PostgreSqlSchemaManager extends AbstractSchemaManager
34
{
35
    /**
36
     * @var array
37
     */
38
    private $existingSchemaPaths;
39
40
    /**
41
     * Gets all the existing schema names.
42
     *
43
     * @return array
44
     */
45
    public function getSchemaNames()
46
    {
47
        $rows = $this->_conn->fetchAll("SELECT nspname as schema_name FROM pg_namespace WHERE nspname !~ '^pg_.*' and nspname != 'information_schema'");
48
49
        return array_map(function ($v) { return $v['schema_name']; }, $rows);
50
    }
51
52
    /**
53
     * Returns an array of schema search paths.
54
     *
55
     * This is a PostgreSQL only function.
56
     *
57
     * @return array
58
     */
59
    public function getSchemaSearchPaths()
60
    {
61
        $params = $this->_conn->getParams();
62
        $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path'));
63
64
        if (isset($params['user'])) {
65
            $schema = str_replace('"$user"', $params['user'], $schema);
66
        }
67
68
        return array_map('trim', $schema);
69
    }
70
71
    /**
72
     * Gets names of all existing schemas in the current users search path.
73
     *
74
     * This is a PostgreSQL only function.
75
     *
76
     * @return array
77
     */
78
    public function getExistingSchemaSearchPaths()
79
    {
80
        if ($this->existingSchemaPaths === null) {
81
            $this->determineExistingSchemaSearchPaths();
82
        }
83
84
        return $this->existingSchemaPaths;
85
    }
86
87
    /**
88
     * Sets or resets the order of the existing schemas in the current search path of the user.
89
     *
90
     * This is a PostgreSQL only function.
91
     *
92
     * @return void
93
     */
94
    public function determineExistingSchemaSearchPaths()
95
    {
96
        $names = $this->getSchemaNames();
97
        $paths = $this->getSchemaSearchPaths();
98
99
        $this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
100
            return in_array($v, $names);
101
        });
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function dropDatabase($database)
108
    {
109
        try {
110
            parent::dropDatabase($database);
111
        } catch (DriverException $exception) {
112
            // If we have a SQLSTATE 55006, the drop database operation failed
113
            // because of active connections on the database.
114
            // To force dropping the database, we first have to close all active connections
115
            // on that database and issue the drop database operation again.
116
            if ($exception->getSQLState() !== '55006') {
117
                throw $exception;
118
            }
119
120
            $this->_execSql(
121
                [
122
                    $this->_platform->getDisallowDatabaseConnectionsSQL($database),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\DBAL\Platforms\AbstractPlatform as the method getDisallowDatabaseConnectionsSQL() does only exist in the following sub-classes of Doctrine\DBAL\Platforms\AbstractPlatform: Doctrine\DBAL\Platforms\PostgreSQL91Platform, Doctrine\DBAL\Platforms\PostgreSQL92Platform, Doctrine\DBAL\Platforms\PostgreSQL94Platform, Doctrine\DBAL\Platforms\PostgreSqlPlatform. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
123
                    $this->_platform->getCloseActiveDatabaseConnectionsSQL($database),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\DBAL\Platforms\AbstractPlatform as the method getCloseActiveDatabaseConnectionsSQL() does only exist in the following sub-classes of Doctrine\DBAL\Platforms\AbstractPlatform: Doctrine\DBAL\Platforms\PostgreSQL91Platform, Doctrine\DBAL\Platforms\PostgreSQL92Platform, Doctrine\DBAL\Platforms\PostgreSQL94Platform, Doctrine\DBAL\Platforms\PostgreSqlPlatform. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
124
                ]
125
            );
126
127
            parent::dropDatabase($database);
128
        }
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    protected function _getPortableTableForeignKeyDefinition($tableForeignKey)
135
    {
136
        $onUpdate = null;
137
        $onDelete = null;
138
139
        if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
140
            $onUpdate = $match[1];
141
        }
142
        if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) {
143
            $onDelete = $match[1];
144
        }
145
146
        if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) {
147
            // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get
148
            // the idea to trim them here.
149
            $localColumns = array_map('trim', explode(",", $values[1]));
150
            $foreignColumns = array_map('trim', explode(",", $values[3]));
151
            $foreignTable = $values[2];
152
        }
153
154
        return new ForeignKeyConstraint(
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\DBA...Delete' => $onDelete)); (Doctrine\DBAL\Schema\ForeignKeyConstraint) is incompatible with the return type of the parent method Doctrine\DBAL\Schema\Abs...bleForeignKeyDefinition of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
155
            $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'],
0 ignored issues
show
Bug introduced by
The variable $localColumns does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $foreignTable does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $foreignColumns does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
156
            ['onUpdate' => $onUpdate, 'onDelete' => $onDelete]
157
        );
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163
    protected function _getPortableTriggerDefinition($trigger)
164
    {
165
        return $trigger['trigger_name'];
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    protected function _getPortableViewDefinition($view)
172
    {
173
        return new View($view['schemaname'].'.'.$view['viewname'], $view['definition']);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\DBA..., $view['definition']); (Doctrine\DBAL\Schema\View) is incompatible with the return type of the parent method Doctrine\DBAL\Schema\Abs...tPortableViewDefinition of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    protected function _getPortableUserDefinition($user)
180
    {
181
        return [
182
            'user' => $user['usename'],
183
            'password' => $user['passwd']
184
        ];
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    protected function _getPortableTableDefinition($table)
191
    {
192
        $schemas = $this->getExistingSchemaSearchPaths();
193
        $firstSchema = array_shift($schemas);
194
195
        if ($table['schema_name'] == $firstSchema) {
196
            return $table['table_name'];
197
        } else {
198
            return $table['schema_name'] . "." . $table['table_name'];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $table['schema_na.... $table['table_name']; (string) is incompatible with the return type of the parent method Doctrine\DBAL\Schema\Abs...PortableTableDefinition of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
199
        }
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     *
205
     * @license New BSD License
206
     * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html
207
     */
208
    protected function _getPortableTableIndexesList($tableIndexes, $tableName=null)
209
    {
210
        $buffer = [];
211
        foreach ($tableIndexes as $row) {
212
            $colNumbers = explode(' ', $row['indkey']);
213
            $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )';
214
            $columnNameSql = "SELECT attnum, attname FROM pg_attribute
215
                WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;";
216
217
            $stmt = $this->_conn->executeQuery($columnNameSql);
218
            $indexColumns = $stmt->fetchAll();
219
220
            // required for getting the order of the columns right.
221
            foreach ($colNumbers as $colNum) {
222
                foreach ($indexColumns as $colRow) {
223
                    if ($colNum == $colRow['attnum']) {
224
                        $buffer[] = [
225
                            'key_name' => $row['relname'],
226
                            'column_name' => trim($colRow['attname']),
227
                            'non_unique' => !$row['indisunique'],
228
                            'primary' => $row['indisprimary'],
229
                            'where' => $row['where'],
230
                        ];
231
                    }
232
                }
233
            }
234
        }
235
236
        return parent::_getPortableTableIndexesList($buffer, $tableName);
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242
    protected function _getPortableDatabaseDefinition($database)
243
    {
244
        return $database['datname'];
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250 1
    protected function _getPortableSequencesList($sequences)
251
    {
252 1
        $sequenceDefinitions = [];
253
254 1
        foreach ($sequences as $sequence) {
255 1
            if ($sequence['schemaname'] != 'public') {
256 1
                $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
257
            } else {
258
                $sequenceName = $sequence['relname'];
259
            }
260
261 1
            $sequenceDefinitions[$sequenceName] = $sequence;
262
        }
263
264 1
        $list = [];
265
266 1
        foreach ($this->filterAssetNames(array_keys($sequenceDefinitions)) as $sequenceName) {
267 1
            $list[] = $this->_getPortableSequenceDefinition($sequenceDefinitions[$sequenceName]);
268
        }
269
270 1
        return $list;
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276
    protected function getPortableNamespaceDefinition(array $namespace)
277
    {
278
        return $namespace['nspname'];
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284 1
    protected function _getPortableSequenceDefinition($sequence)
285
    {
286 1
        if ($sequence['schemaname'] != 'public') {
287 1
            $sequenceName = $sequence['schemaname'] . "." . $sequence['relname'];
288
        } else {
289
            $sequenceName = $sequence['relname'];
290
        }
291
292 1
        $data = $this->_conn->fetchAll('SELECT min_value, increment_by FROM ' . $this->_platform->quoteIdentifier($sequenceName));
293
294 1
        return new Sequence($sequenceName, $data[0]['increment_by'], $data[0]['min_value']);
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300
    protected function _getPortableTableColumnDefinition($tableColumn)
301
    {
302
        $tableColumn = array_change_key_case($tableColumn, CASE_LOWER);
303
304
        if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') {
305
            // get length from varchar definition
306
            $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']);
307
            $tableColumn['length'] = $length;
308
        }
309
310
        $matches = [];
311
312
        $autoincrement = false;
313
        if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) {
314
            $tableColumn['sequence'] = $matches[1];
315
            $tableColumn['default'] = null;
316
            $autoincrement = true;
317
        }
318
319
        if (preg_match("/^['(](.*)[')]::.*$/", $tableColumn['default'], $matches)) {
320
            $tableColumn['default'] = $matches[1];
321
        }
322
323
        if (stripos($tableColumn['default'], 'NULL') === 0) {
324
            $tableColumn['default'] = null;
325
        }
326
327
        $length = (isset($tableColumn['length'])) ? $tableColumn['length'] : null;
328
        if ($length == '-1' && isset($tableColumn['atttypmod'])) {
329
            $length = $tableColumn['atttypmod'] - 4;
330
        }
331
        if ((int) $length <= 0) {
332
            $length = null;
333
        }
334
        $fixed = null;
335
336
        if (!isset($tableColumn['name'])) {
337
            $tableColumn['name'] = '';
338
        }
339
340
        $precision = null;
341
        $scale = null;
342
        $jsonb = null;
343
344
        $dbType = strtolower($tableColumn['type']);
345
        if (strlen($tableColumn['domain_type']) && !$this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) {
346
            $dbType = strtolower($tableColumn['domain_type']);
347
            $tableColumn['complete_type'] = $tableColumn['domain_complete_type'];
348
        }
349
350
        $type = $this->_platform->getDoctrineTypeMapping($dbType);
351
        $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type);
352
        $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type);
353
354
        switch ($dbType) {
355
            case 'smallint':
356
            case 'int2':
357
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
358
                $length = null;
359
                break;
360
            case 'int':
361
            case 'int4':
362
            case 'integer':
363
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
364
                $length = null;
365
                break;
366
            case 'bigint':
367
            case 'int8':
368
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
369
                $length = null;
370
                break;
371
            case 'bool':
372
            case 'boolean':
373
                if ($tableColumn['default'] === 'true') {
374
                    $tableColumn['default'] = true;
375
                }
376
377
                if ($tableColumn['default'] === 'false') {
378
                    $tableColumn['default'] = false;
379
                }
380
381
                $length = null;
382
                break;
383
            case 'text':
384
            case '_varchar':
385
            case 'varchar':
386
            $tableColumn['default'] = $this->fixDefaultValueQuotes($tableColumn['default']);
387
                $fixed = false;
388
                break;
389
            case 'interval':
390
                $fixed = false;
391
                break;
392
            case 'char':
393
            case 'bpchar':
394
                $fixed = true;
395
                break;
396
            case 'float':
397
            case 'float4':
398
            case 'float8':
399
            case 'double':
400
            case 'double precision':
401
            case 'real':
402
            case 'decimal':
403
            case 'money':
404
            case 'numeric':
405
                $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']);
406
407
                if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) {
408
                    $precision = $match[1];
409
                    $scale = $match[2];
410
                    $length = null;
411
                }
412
                break;
413
            case 'year':
414
                $length = null;
415
                break;
416
417
            // PostgreSQL 9.4+ only
418
            case 'jsonb':
419
                $jsonb = true;
420
                break;
421
        }
422
423
        if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) {
424
            $tableColumn['default'] = $match[1];
425
        }
426
427
        $options = [
428
            'length'        => $length,
429
            'notnull'       => (bool) $tableColumn['isnotnull'],
430
            'default'       => $tableColumn['default'],
431
            'primary'       => (bool) ($tableColumn['pri'] == 't'),
432
            'precision'     => $precision,
433
            'scale'         => $scale,
434
            'fixed'         => $fixed,
435
            'unsigned'      => false,
436
            'autoincrement' => $autoincrement,
437
            'comment'       => isset($tableColumn['comment']) && $tableColumn['comment'] !== ''
438
                ? $tableColumn['comment']
439
                : null,
440
        ];
441
442
        $column = new Column($tableColumn['field'], Type::getType($type), $options);
443
444
        if (isset($tableColumn['collation']) && !empty($tableColumn['collation'])) {
445
            $column->setPlatformOption('collation', $tableColumn['collation']);
446
        }
447
448
        if (in_array($column->getType()->getName(), [Type::JSON_ARRAY, Type::JSON], true)) {
449
            $column->setPlatformOption('jsonb', $jsonb);
450
        }
451
452
        return $column;
453
    }
454
455
    /**
456
     * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually.
457
     *
458
     * @param mixed $defaultValue
459
     *
460
     * @return mixed
461
     */
462
    private function fixVersion94NegativeNumericDefaultValue($defaultValue)
463
    {
464
        if (strpos($defaultValue, '(') === 0) {
465
            return trim($defaultValue, '()');
466
        }
467
468
        return $defaultValue;
469
    }
470
471
    /**
472
     * Un-escape a default value as given by PostgreSQL
473
     *
474
     * @param null|string $default
475
     *
476
     * @return null|string
477
     */
478
    private function fixDefaultValueQuotes(?string $default): ?string
479
    {
480
        if ($default === null) {
481
            return $default;
482
        }
483
484
        $default = str_replace("\\\\", "\\", $default);
485
        $default = str_replace("''", "'", $default);
486
487
        return $default;
488
    }
489
}
490