Passed
Push — 2.x ( a18c78...0f4623 )
by Aleksei
41:02 queued 21:04
created

PostgresTable::fetchReferences()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.25

Importance

Changes 0
Metric Value
cc 4
eloc 28
c 0
b 0
f 0
nc 6
nop 0
dl 0
loc 40
ccs 9
cts 12
cp 0.75
crap 4.25
rs 9.472
1
<?php
2
3
/**
4
 * This file is part of Cycle ORM package.
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace Cycle\Database\Driver\Postgres\Schema;
13
14
use Cycle\Database\Driver\HandlerInterface;
15
use Cycle\Database\Driver\Postgres\PostgresDriver;
16
use Cycle\Database\Schema\AbstractColumn;
0 ignored issues
show
Bug introduced by
The type Cycle\Database\Schema\AbstractColumn 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...
17
use Cycle\Database\Schema\AbstractForeignKey;
18
use Cycle\Database\Schema\AbstractIndex;
19
use Cycle\Database\Schema\AbstractTable;
20
21
/**
22
 * @property PostgresDriver $driver
23
 *
24
 * @method PostgresColumn[] getColumns()
25
 */
26
class PostgresTable extends AbstractTable
27
{
28
    /**
29
     * Found table sequences.
30
     */
31
    private array $sequences = [];
32
33
    /**
34
     * Sequence object name usually defined only for primary keys and required by ORM to correctly
35
     * resolve inserted row id.
36
     */
37
    private ?string $primarySequence = null;
38
39
    /**
40
     * Sequence object name usually defined only for primary keys and required by ORM to correctly
41
     * resolve inserted row id.
42
     */
43
    public function getSequence(): ?string
44
    {
45
        return $this->primarySequence;
46 6
    }
47
48 6
    public function getName(): string
49
    {
50
        return $this->removeSchemaFromTableName($this->getFullName());
51
    }
52
53
    /**
54 516
     * SQLServer will reload schemas after successful save.
55
     */
56 516
    public function save(int $operation = HandlerInterface::DO_ALL, bool $reset = true): void
57
    {
58 516
        parent::save($operation, $reset);
59 516
60 516
        if ($reset) {
61 516
            foreach ($this->fetchColumns() as $column) {
62
                $currentColumn = $this->current->findColumn($column->getName());
63 516
                if ($currentColumn !== null && $column->compare($currentColumn)) {
64
                    //Ensure constrained columns
65
                    $this->current->registerColumn($column);
66
                }
67 516
            }
68
        }
69 516
    }
70
71 516
    public function getDependencies(): array
72
    {
73
        $tables = [];
74 516
        foreach ($this->current->getForeignKeys() as $foreignKey) {
75 516
            [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($foreignKey->getForeignTable());
76
            $tables[] = $tableSchema . '.' . $tableName;
77
        }
78
79
        return $tables;
80
    }
81 516
82 516
    protected function fetchColumns(): array
83
    {
84 516
        [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName());
85 516
86
        //Required for constraints fetch
87
        $tableOID = $this->driver->query(
88
            'SELECT pgc.oid
89
                FROM pg_class as pgc
90
                JOIN pg_namespace as pgn
91 516
                    ON (pgn.oid = pgc.relnamespace)
92
                WHERE pgn.nspname = ?
93
                AND pgc.relname = ?',
94 516
            [$tableSchema, $tableName],
95 516
        )->fetchColumn();
96 516
97
        $query = $this->driver->query(
98 516
            'SELECT columns.*, pg_type.*, pg_description.description
99 438
               FROM information_schema.columns
100 516
               JOIN pg_catalog.pg_type
101 438
                   ON (pg_type.typname = columns.udt_name)
102
               JOIN pg_catalog.pg_statio_all_tables
103
	               ON (pg_statio_all_tables.relname = columns.table_name
104
	               AND pg_statio_all_tables.schemaname = columns.table_schema)
105
               LEFT JOIN pg_catalog.pg_description
106 366
	               ON (pg_description.objoid = pg_statio_all_tables.relid
107
	               AND pg_description.objsubid = columns.ordinal_position)
108
               WHERE columns.table_schema = ?
109 516
               AND columns.table_name = ?',
110 516
            [$tableSchema, $tableName],
111 516
        );
112 516
113
        $primaryKeys = \array_column($this->driver->query(
114
            'SELECT key_column_usage.column_name
115
                FROM information_schema.table_constraints
116 516
                JOIN information_schema.key_column_usage
117
                    ON (
118
                            key_column_usage.table_name = table_constraints.table_name AND
119 508
                            key_column_usage.table_schema = table_constraints.table_schema AND
120
                            key_column_usage.constraint_name = table_constraints.constraint_name
121 508
                        )
122
                WHERE table_constraints.constraint_type = \'PRIMARY KEY\' AND
123 508
                      key_column_usage.ordinal_position IS NOT NULL AND
124
                      table_constraints.table_schema = ? AND
125
                      table_constraints.table_name = ?',
126
            [$tableSchema, $tableName],
127
        )->fetchAll(), 'column_name');
128
129
        $result = [];
130
        foreach ($query->fetchAll() as $schema) {
131
            $name = $schema['column_name'];
132
            if (
133
                \is_string($schema['column_default'])
134 508
                && \preg_match(
135 508
                    '/^nextval\([\'"]([a-z0-9_"]+)[\'"](?:::regclass)?\)$/i',
136 364
                    $schema['column_default'],
137
                    $matches,
138 364
                )
139
            ) {
140 116
                //Column is sequential
141
                $this->sequences[$name] = $matches[1];
142
            }
143 508
144
            $schema['is_primary'] = \in_array($schema['column_name'], $primaryKeys, true);
145
146 508
            $result[] = PostgresColumn::createInstance(
147
                $tableSchema . '.' . $tableName,
148 508
                $schema + ['tableOID' => $tableOID],
149
                $this->driver,
150
            );
151 508
        }
152
153
        return $result;
154
    }
155
156
    protected function fetchIndexes(bool $all = false): array
157
    {
158
        [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName());
159
160
        $query = <<<SQL
161
            SELECT i.indexname, i.indexdef, c.contype
162
            FROM pg_indexes i
163 508
            LEFT JOIN pg_namespace ns
164 508
                ON nspname = i.schemaname
165 56
            LEFT JOIN pg_constraint c
166 56
                ON c.conname = i.indexname
167 56
                AND c.connamespace = ns.oid
168 56
            WHERE i.schemaname = ? AND i.tablename = ?
169 56
            SQL;
170
171
        $result = [];
172 2
        foreach ($this->driver->query($query, [$tableSchema, $tableName]) as $schema) {
173 2
            if ($schema['contype'] === 'p') {
174
                //Skipping primary keys
175
                continue;
176 508
            }
177 508
            $result[] = PostgresIndex::createInstance($tableSchema . '.' . $tableName, $schema);
178 56
        }
179 56
180 56
        return $result;
181
    }
182
183
    protected function fetchReferences(): array
184
    {
185 508
        [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName());
186
187
        //Mindblowing
188 508
        $query = 'SELECT tc.constraint_name, tc.constraint_schema, tc.table_name, kcu.column_name, rc.update_rule, '
189
            . 'rc.delete_rule, ccu.table_name AS foreign_table_name, '
190 508
            . "ccu.column_name AS foreign_column_name\n"
191
            . "FROM information_schema.table_constraints AS tc\n"
192 508
            . "JOIN information_schema.key_column_usage AS kcu\n"
193
            . "   ON tc.constraint_name = kcu.constraint_name\n"
194
            . "JOIN information_schema.constraint_column_usage AS ccu\n"
195
            . "   ON ccu.constraint_name = tc.constraint_name\n"
196
            . "JOIN information_schema.referential_constraints AS rc\n"
197
            . "   ON rc.constraint_name = tc.constraint_name\n"
198
            . "WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = ? AND tc.table_name = ?";
199
200
        $fks = [];
201
        foreach ($this->driver->query($query, [$tableSchema, $tableName]) as $schema) {
202
            if (!isset($fks[$schema['constraint_name']])) {
203
                $fks[$schema['constraint_name']] = $schema;
204 508
                $fks[$schema['constraint_name']]['column_name'] = [$schema['column_name']];
205
                $fks[$schema['constraint_name']]['foreign_column_name'] = [$schema['foreign_column_name']];
206 364
                continue;
207
            }
208 364
209
            $fks[$schema['constraint_name']]['column_name'][] = $schema['column_name'];
210
            $fks[$schema['constraint_name']]['foreign_column_name'][] = $schema['foreign_column_name'];
211
        }
212
213
        $result = [];
214
        foreach ($fks as $schema) {
215
            $result[] = PostgresForeignKey::createInstance(
216
                $tableSchema . '.' . $tableName,
217 364
                $this->getPrefix(),
218
                $schema,
219
            );
220 148
        }
221
222
        return $result;
223
    }
224
225
    protected function fetchPrimaryKeys(): array
226 514
    {
227
        [$tableSchema, $tableName] = $this->driver->parseSchemaAndTable($this->getFullName());
228 514
229 514
        $query = <<<SQL
230 514
            SELECT i.indexname, i.indexdef, c.contype
231 514
            FROM pg_indexes i
232
            INNER JOIN pg_namespace ns
233
                ON nspname = i.schemaname
234
            INNER JOIN pg_constraint c
235
                ON c.conname = i.indexname
236
                AND c.connamespace = ns.oid
237
            WHERE i.schemaname = ? AND i.tablename = ?
238 116
              AND c.contype = 'p'
239
            SQL;
240 116
241 116
        foreach ($this->driver->query($query, [$tableSchema, $tableName]) as $schema) {
242 116
            //To simplify definitions
243
            $index = PostgresIndex::createInstance($tableSchema . '.' . $tableName, $schema);
244
245
            if (\is_array($this->primarySequence) && \count($index->getColumns()) === 1) {
246
                $column = $index->getColumns()[0];
247
248
                if (isset($this->sequences[$column])) {
249 56
                    //We found our primary sequence
250
                    $this->primarySequence = $this->sequences[$column];
251 56
                }
252 56
            }
253 56
254 56
            return $index->getColumns();
255
        }
256
257
        return [];
258
    }
259
260
    /**
261 522
     * @psalm-param non-empty-string $name
262
     */
263 522
    protected function createColumn(string $name): AbstractColumn
264
    {
265 522
        return new PostgresColumn(
266
            $this->getNormalizedTableName(),
267
            $this->removeSchemaFromTableName($name),
268
            $this->driver->getTimezone(),
269
        );
270
    }
271 514
272
    /**
273 514
     * @psalm-param non-empty-string $name
274
     */
275 514
    protected function createIndex(string $name): AbstractIndex
276
    {
277
        return new PostgresIndex(
278
            $this->getNormalizedTableName(),
279
            $this->removeSchemaFromTableName($name),
280
        );
281
    }
282
283 514
    /**
284
     * @psalm-param non-empty-string $name
285 514
     */
286 122
    protected function createForeign(string $name): AbstractForeignKey
287
    {
288
        return new PostgresForeignKey(
289 514
            $this->getNormalizedTableName(),
290
            $this->getPrefix(),
291
            $this->removeSchemaFromTableName($name),
292
        );
293
    }
294
295
    /**
296
     * @psalm-param non-empty-string $name
297
     */
298
    protected function prefixTableName(string $name): string
299
    {
300
        [$schema, $name] = $this->driver->parseSchemaAndTable($name);
301
302
        return $schema . '.' . parent::prefixTableName($name);
303
    }
304
305
    /**
306
     * Get table name with schema. If table doesn't contain schema, schema will be added from config
307
     */
308
    protected function getNormalizedTableName(): string
309
    {
310
        [$schema, $name] = $this->driver->parseSchemaAndTable($this->getFullName());
311
312
        return $schema . '.' . $name;
313
    }
314
315
    /**
316
     * Return table name without schema
317
     *
318
     * @psalm-param non-empty-string $name
319
     */
320
    protected function removeSchemaFromTableName(string $name): string
321
    {
322
        if (\str_contains($name, '.')) {
323
            [, $name] = \explode('.', $name, 2);
324
        }
325
326
        return $name;
327
    }
328
}
329