summaryrefslogtreecommitdiff
path: root/vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php')
-rw-r--r--vendor/doctrine/dbal/src/Schema/SQLServerSchemaManager.php498
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
3declare(strict_types=1);
4
5namespace Doctrine\DBAL\Schema;
6
7use Doctrine\DBAL\Exception;
8use Doctrine\DBAL\Platforms\SQLServer;
9use Doctrine\DBAL\Platforms\SQLServerPlatform;
10use Doctrine\DBAL\Result;
11use Doctrine\DBAL\Types\Type;
12
13use function array_change_key_case;
14use function assert;
15use function explode;
16use function implode;
17use function is_string;
18use function preg_match;
19use function sprintf;
20use function str_contains;
21use function str_replace;
22use function strtok;
23
24use const CASE_LOWER;
25
26/**
27 * SQL Server Schema Manager.
28 *
29 * @extends AbstractSchemaManager<SQLServerPlatform>
30 */
31class 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'
42SELECT name
43FROM sys.schemas
44WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')
45SQL,
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'
292SELECT name AS table_name,
293 SCHEMA_NAME(schema_id) AS schema_name
294FROM sys.objects
295WHERE type = 'U'
296 AND name != 'sysdiagrams'
297ORDER BY name
298SQL;
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'
338SQL;
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
382SQL;
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
419SQL;
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
451SQL;
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}