Passed
Push — 2.x ( 987591...3b051d )
by Aleksei
17:06
created

PostgresTable::getDependencies()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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