diff options
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php')
-rw-r--r-- | vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php b/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php new file mode 100644 index 0000000..e0a74ce --- /dev/null +++ b/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php | |||
@@ -0,0 +1,498 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\DBAL\Schema; | ||
6 | |||
7 | use Doctrine\DBAL\Exception; | ||
8 | use Doctrine\DBAL\Platforms\SQLServer; | ||
9 | use Doctrine\DBAL\Platforms\SQLServerPlatform; | ||
10 | use Doctrine\DBAL\Result; | ||
11 | use Doctrine\DBAL\Types\Type; | ||
12 | |||
13 | use function array_change_key_case; | ||
14 | use function assert; | ||
15 | use function explode; | ||
16 | use function implode; | ||
17 | use function is_string; | ||
18 | use function preg_match; | ||
19 | use function sprintf; | ||
20 | use function str_contains; | ||
21 | use function str_replace; | ||
22 | use function strtok; | ||
23 | |||
24 | use const CASE_LOWER; | ||
25 | |||
26 | /** | ||
27 | * SQL Server Schema Manager. | ||
28 | * | ||
29 | * @extends AbstractSchemaManager<SQLServerPlatform> | ||
30 | */ | ||
31 | class SQLServerSchemaManager extends AbstractSchemaManager | ||
32 | { | ||
33 | private ?string $databaseCollation = null; | ||
34 | |||
35 | /** | ||
36 | * {@inheritDoc} | ||
37 | */ | ||
38 | public function listSchemaNames(): array | ||
39 | { | ||
40 | return $this->connection->fetchFirstColumn( | ||
41 | <<<'SQL' | ||
42 | SELECT name | ||
43 | FROM sys.schemas | ||
44 | WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys') | ||
45 | SQL, | ||
46 | ); | ||
47 | } | ||
48 | |||
49 | /** | ||
50 | * {@inheritDoc} | ||
51 | */ | ||
52 | protected function _getPortableSequenceDefinition(array $sequence): Sequence | ||
53 | { | ||
54 | return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']); | ||
55 | } | ||
56 | |||
57 | /** | ||
58 | * {@inheritDoc} | ||
59 | */ | ||
60 | protected function _getPortableTableColumnDefinition(array $tableColumn): Column | ||
61 | { | ||
62 | $dbType = strtok($tableColumn['type'], '(), '); | ||
63 | assert(is_string($dbType)); | ||
64 | |||
65 | $length = (int) $tableColumn['length']; | ||
66 | |||
67 | $precision = null; | ||
68 | |||
69 | $scale = 0; | ||
70 | $fixed = false; | ||
71 | |||
72 | if (! isset($tableColumn['name'])) { | ||
73 | $tableColumn['name'] = ''; | ||
74 | } | ||
75 | |||
76 | if ($tableColumn['scale'] !== null) { | ||
77 | $scale = (int) $tableColumn['scale']; | ||
78 | } | ||
79 | |||
80 | if ($tableColumn['precision'] !== null) { | ||
81 | $precision = (int) $tableColumn['precision']; | ||
82 | } | ||
83 | |||
84 | switch ($dbType) { | ||
85 | case 'nchar': | ||
86 | case 'ntext': | ||
87 | // Unicode data requires 2 bytes per character | ||
88 | $length /= 2; | ||
89 | break; | ||
90 | |||
91 | case 'nvarchar': | ||
92 | if ($length === -1) { | ||
93 | break; | ||
94 | } | ||
95 | |||
96 | // Unicode data requires 2 bytes per character | ||
97 | $length /= 2; | ||
98 | break; | ||
99 | |||
100 | case 'varchar': | ||
101 | // TEXT type is returned as VARCHAR(MAX) with a length of -1 | ||
102 | if ($length === -1) { | ||
103 | $dbType = 'text'; | ||
104 | } | ||
105 | |||
106 | break; | ||
107 | |||
108 | case 'varbinary': | ||
109 | if ($length === -1) { | ||
110 | $dbType = 'blob'; | ||
111 | } | ||
112 | |||
113 | break; | ||
114 | } | ||
115 | |||
116 | if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') { | ||
117 | $fixed = true; | ||
118 | } | ||
119 | |||
120 | $type = $this->platform->getDoctrineTypeMapping($dbType); | ||
121 | |||
122 | $options = [ | ||
123 | 'fixed' => $fixed, | ||
124 | 'notnull' => (bool) $tableColumn['notnull'], | ||
125 | 'scale' => $scale, | ||
126 | 'precision' => $precision, | ||
127 | 'autoincrement' => (bool) $tableColumn['autoincrement'], | ||
128 | ]; | ||
129 | |||
130 | if (isset($tableColumn['comment'])) { | ||
131 | $options['comment'] = $tableColumn['comment']; | ||
132 | } | ||
133 | |||
134 | if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) { | ||
135 | $options['length'] = $length; | ||
136 | } | ||
137 | |||
138 | $column = new Column($tableColumn['name'], Type::getType($type), $options); | ||
139 | |||
140 | if ($tableColumn['default'] !== null) { | ||
141 | $default = $this->parseDefaultExpression($tableColumn['default']); | ||
142 | |||
143 | $column->setDefault($default); | ||
144 | $column->setPlatformOption( | ||
145 | SQLServerPlatform::OPTION_DEFAULT_CONSTRAINT_NAME, | ||
146 | $tableColumn['df_name'], | ||
147 | ); | ||
148 | } | ||
149 | |||
150 | if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') { | ||
151 | $column->setPlatformOption('collation', $tableColumn['collation']); | ||
152 | } | ||
153 | |||
154 | return $column; | ||
155 | } | ||
156 | |||
157 | private function parseDefaultExpression(string $value): ?string | ||
158 | { | ||
159 | while (preg_match('/^\((.*)\)$/s', $value, $matches)) { | ||
160 | $value = $matches[1]; | ||
161 | } | ||
162 | |||
163 | if ($value === 'NULL') { | ||
164 | return null; | ||
165 | } | ||
166 | |||
167 | if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) { | ||
168 | $value = str_replace("''", "'", $matches[1]); | ||
169 | } | ||
170 | |||
171 | if ($value === 'getdate()') { | ||
172 | return $this->platform->getCurrentTimestampSQL(); | ||
173 | } | ||
174 | |||
175 | return $value; | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * {@inheritDoc} | ||
180 | */ | ||
181 | protected function _getPortableTableForeignKeysList(array $tableForeignKeys): array | ||
182 | { | ||
183 | $foreignKeys = []; | ||
184 | |||
185 | foreach ($tableForeignKeys as $tableForeignKey) { | ||
186 | $name = $tableForeignKey['ForeignKey']; | ||
187 | |||
188 | if (! isset($foreignKeys[$name])) { | ||
189 | $foreignKeys[$name] = [ | ||
190 | 'local_columns' => [$tableForeignKey['ColumnName']], | ||
191 | 'foreign_table' => $tableForeignKey['ReferenceTableName'], | ||
192 | 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']], | ||
193 | 'name' => $name, | ||
194 | 'options' => [ | ||
195 | 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), | ||
196 | 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), | ||
197 | ], | ||
198 | ]; | ||
199 | } else { | ||
200 | $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName']; | ||
201 | $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName']; | ||
202 | } | ||
203 | } | ||
204 | |||
205 | return parent::_getPortableTableForeignKeysList($foreignKeys); | ||
206 | } | ||
207 | |||
208 | /** | ||
209 | * {@inheritDoc} | ||
210 | */ | ||
211 | protected function _getPortableTableIndexesList(array $tableIndexes, string $tableName): array | ||
212 | { | ||
213 | foreach ($tableIndexes as &$tableIndex) { | ||
214 | $tableIndex['non_unique'] = (bool) $tableIndex['non_unique']; | ||
215 | $tableIndex['primary'] = (bool) $tableIndex['primary']; | ||
216 | $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null; | ||
217 | } | ||
218 | |||
219 | return parent::_getPortableTableIndexesList($tableIndexes, $tableName); | ||
220 | } | ||
221 | |||
222 | /** | ||
223 | * {@inheritDoc} | ||
224 | */ | ||
225 | protected function _getPortableTableForeignKeyDefinition(array $tableForeignKey): ForeignKeyConstraint | ||
226 | { | ||
227 | return new ForeignKeyConstraint( | ||
228 | $tableForeignKey['local_columns'], | ||
229 | $tableForeignKey['foreign_table'], | ||
230 | $tableForeignKey['foreign_columns'], | ||
231 | $tableForeignKey['name'], | ||
232 | $tableForeignKey['options'], | ||
233 | ); | ||
234 | } | ||
235 | |||
236 | /** | ||
237 | * {@inheritDoc} | ||
238 | */ | ||
239 | protected function _getPortableTableDefinition(array $table): string | ||
240 | { | ||
241 | if ($table['schema_name'] !== 'dbo') { | ||
242 | return $table['schema_name'] . '.' . $table['table_name']; | ||
243 | } | ||
244 | |||
245 | return $table['table_name']; | ||
246 | } | ||
247 | |||
248 | /** | ||
249 | * {@inheritDoc} | ||
250 | */ | ||
251 | protected function _getPortableDatabaseDefinition(array $database): string | ||
252 | { | ||
253 | return $database['name']; | ||
254 | } | ||
255 | |||
256 | /** | ||
257 | * {@inheritDoc} | ||
258 | */ | ||
259 | protected function _getPortableViewDefinition(array $view): View | ||
260 | { | ||
261 | return new View($view['name'], $view['definition']); | ||
262 | } | ||
263 | |||
264 | /** @throws Exception */ | ||
265 | public function createComparator(): Comparator | ||
266 | { | ||
267 | return new SQLServer\Comparator($this->platform, $this->getDatabaseCollation()); | ||
268 | } | ||
269 | |||
270 | /** @throws Exception */ | ||
271 | private function getDatabaseCollation(): string | ||
272 | { | ||
273 | if ($this->databaseCollation === null) { | ||
274 | $databaseCollation = $this->connection->fetchOne( | ||
275 | 'SELECT collation_name FROM sys.databases WHERE name = ' | ||
276 | . $this->platform->getCurrentDatabaseExpression(), | ||
277 | ); | ||
278 | |||
279 | // a database is always selected, even if omitted in the connection parameters | ||
280 | assert(is_string($databaseCollation)); | ||
281 | |||
282 | $this->databaseCollation = $databaseCollation; | ||
283 | } | ||
284 | |||
285 | return $this->databaseCollation; | ||
286 | } | ||
287 | |||
288 | protected function selectTableNames(string $databaseName): Result | ||
289 | { | ||
290 | // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams | ||
291 | $sql = <<<'SQL' | ||
292 | SELECT name AS table_name, | ||
293 | SCHEMA_NAME(schema_id) AS schema_name | ||
294 | FROM sys.objects | ||
295 | WHERE type = 'U' | ||
296 | AND name != 'sysdiagrams' | ||
297 | ORDER BY name | ||
298 | SQL; | ||
299 | |||
300 | return $this->connection->executeQuery($sql); | ||
301 | } | ||
302 | |||
303 | protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result | ||
304 | { | ||
305 | $sql = 'SELECT'; | ||
306 | |||
307 | if ($tableName === null) { | ||
308 | $sql .= ' obj.name AS table_name, scm.name AS schema_name,'; | ||
309 | } | ||
310 | |||
311 | $sql .= <<<'SQL' | ||
312 | col.name, | ||
313 | type.name AS type, | ||
314 | col.max_length AS length, | ||
315 | ~col.is_nullable AS notnull, | ||
316 | def.definition AS [default], | ||
317 | def.name AS df_name, | ||
318 | col.scale, | ||
319 | col.precision, | ||
320 | col.is_identity AS autoincrement, | ||
321 | col.collation_name AS collation, | ||
322 | -- CAST avoids driver error for sql_variant type | ||
323 | CAST(prop.value AS NVARCHAR(MAX)) AS comment | ||
324 | FROM sys.columns AS col | ||
325 | JOIN sys.types AS type | ||
326 | ON col.user_type_id = type.user_type_id | ||
327 | JOIN sys.objects AS obj | ||
328 | ON col.object_id = obj.object_id | ||
329 | JOIN sys.schemas AS scm | ||
330 | ON obj.schema_id = scm.schema_id | ||
331 | LEFT JOIN sys.default_constraints def | ||
332 | ON col.default_object_id = def.object_id | ||
333 | AND col.object_id = def.parent_object_id | ||
334 | LEFT JOIN sys.extended_properties AS prop | ||
335 | ON obj.object_id = prop.major_id | ||
336 | AND col.column_id = prop.minor_id | ||
337 | AND prop.name = 'MS_Description' | ||
338 | SQL; | ||
339 | |||
340 | // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams | ||
341 | $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"]; | ||
342 | $params = []; | ||
343 | |||
344 | if ($tableName !== null) { | ||
345 | $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name'); | ||
346 | } | ||
347 | |||
348 | $sql .= ' WHERE ' . implode(' AND ', $conditions); | ||
349 | |||
350 | return $this->connection->executeQuery($sql, $params); | ||
351 | } | ||
352 | |||
353 | protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result | ||
354 | { | ||
355 | $sql = 'SELECT'; | ||
356 | |||
357 | if ($tableName === null) { | ||
358 | $sql .= ' tbl.name AS table_name, scm.name AS schema_name,'; | ||
359 | } | ||
360 | |||
361 | $sql .= <<<'SQL' | ||
362 | idx.name AS key_name, | ||
363 | col.name AS column_name, | ||
364 | ~idx.is_unique AS non_unique, | ||
365 | idx.is_primary_key AS [primary], | ||
366 | CASE idx.type | ||
367 | WHEN '1' THEN 'clustered' | ||
368 | WHEN '2' THEN 'nonclustered' | ||
369 | ELSE NULL | ||
370 | END AS flags | ||
371 | FROM sys.tables AS tbl | ||
372 | JOIN sys.schemas AS scm | ||
373 | ON tbl.schema_id = scm.schema_id | ||
374 | JOIN sys.indexes AS idx | ||
375 | ON tbl.object_id = idx.object_id | ||
376 | JOIN sys.index_columns AS idxcol | ||
377 | ON idx.object_id = idxcol.object_id | ||
378 | AND idx.index_id = idxcol.index_id | ||
379 | JOIN sys.columns AS col | ||
380 | ON idxcol.object_id = col.object_id | ||
381 | AND idxcol.column_id = col.column_id | ||
382 | SQL; | ||
383 | |||
384 | $conditions = []; | ||
385 | $params = []; | ||
386 | |||
387 | if ($tableName !== null) { | ||
388 | $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); | ||
389 | $sql .= ' WHERE ' . implode(' AND ', $conditions); | ||
390 | } | ||
391 | |||
392 | $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal'; | ||
393 | |||
394 | return $this->connection->executeQuery($sql, $params); | ||
395 | } | ||
396 | |||
397 | protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result | ||
398 | { | ||
399 | $sql = 'SELECT'; | ||
400 | |||
401 | if ($tableName === null) { | ||
402 | $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,'; | ||
403 | } | ||
404 | |||
405 | $sql .= <<<'SQL' | ||
406 | f.name AS ForeignKey, | ||
407 | SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, | ||
408 | OBJECT_NAME (f.parent_object_id) AS TableName, | ||
409 | COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, | ||
410 | SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, | ||
411 | OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, | ||
412 | COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, | ||
413 | f.delete_referential_action_desc, | ||
414 | f.update_referential_action_desc | ||
415 | FROM sys.foreign_keys AS f | ||
416 | INNER JOIN sys.foreign_key_columns AS fc | ||
417 | INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id | ||
418 | ON f.OBJECT_ID = fc.constraint_object_id | ||
419 | SQL; | ||
420 | |||
421 | $conditions = []; | ||
422 | $params = []; | ||
423 | |||
424 | if ($tableName !== null) { | ||
425 | $conditions[] = $this->getTableWhereClause( | ||
426 | $tableName, | ||
427 | 'SCHEMA_NAME(f.schema_id)', | ||
428 | 'OBJECT_NAME(f.parent_object_id)', | ||
429 | ); | ||
430 | |||
431 | $sql .= ' WHERE ' . implode(' AND ', $conditions); | ||
432 | } | ||
433 | |||
434 | $sql .= ' ORDER BY fc.constraint_column_id'; | ||
435 | |||
436 | return $this->connection->executeQuery($sql, $params); | ||
437 | } | ||
438 | |||
439 | /** | ||
440 | * {@inheritDoc} | ||
441 | */ | ||
442 | protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array | ||
443 | { | ||
444 | $sql = <<<'SQL' | ||
445 | SELECT | ||
446 | tbl.name, | ||
447 | p.value AS [table_comment] | ||
448 | FROM | ||
449 | sys.tables AS tbl | ||
450 | INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 | ||
451 | SQL; | ||
452 | |||
453 | $conditions = ["SCHEMA_NAME(tbl.schema_id) = N'dbo'", "p.name = N'MS_Description'"]; | ||
454 | $params = []; | ||
455 | |||
456 | if ($tableName !== null) { | ||
457 | $conditions[] = "tbl.name = N'" . $tableName . "'"; | ||
458 | } | ||
459 | |||
460 | $sql .= ' WHERE ' . implode(' AND ', $conditions); | ||
461 | |||
462 | /** @var array<string,array<string,mixed>> $metadata */ | ||
463 | $metadata = $this->connection->executeQuery($sql, $params) | ||
464 | ->fetchAllAssociativeIndexed(); | ||
465 | |||
466 | $tableOptions = []; | ||
467 | foreach ($metadata as $table => $data) { | ||
468 | $data = array_change_key_case($data, CASE_LOWER); | ||
469 | |||
470 | $tableOptions[$table] = [ | ||
471 | 'comment' => $data['table_comment'], | ||
472 | ]; | ||
473 | } | ||
474 | |||
475 | return $tableOptions; | ||
476 | } | ||
477 | |||
478 | /** | ||
479 | * Returns the where clause to filter schema and table name in a query. | ||
480 | * | ||
481 | * @param string $table The full qualified name of the table. | ||
482 | * @param string $schemaColumn The name of the column to compare the schema to in the where clause. | ||
483 | * @param string $tableColumn The name of the column to compare the table to in the where clause. | ||
484 | */ | ||
485 | private function getTableWhereClause(string $table, string $schemaColumn, string $tableColumn): string | ||
486 | { | ||
487 | if (str_contains($table, '.')) { | ||
488 | [$schema, $table] = explode('.', $table); | ||
489 | $schema = $this->platform->quoteStringLiteral($schema); | ||
490 | $table = $this->platform->quoteStringLiteral($table); | ||
491 | } else { | ||
492 | $schema = 'SCHEMA_NAME()'; | ||
493 | $table = $this->platform->quoteStringLiteral($table); | ||
494 | } | ||
495 | |||
496 | return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); | ||
497 | } | ||
498 | } | ||