1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @link http://www.yiiframework.com/ |
4
|
|
|
* @copyright Copyright (c) 2008 Yii Software LLC |
5
|
|
|
* @license http://www.yiiframework.com/license/ |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace yii\db\mssql; |
9
|
|
|
|
10
|
|
|
use yii\db\CheckConstraint; |
11
|
|
|
use yii\db\ColumnSchema; |
12
|
|
|
use yii\db\Constraint; |
13
|
|
|
use yii\db\ConstraintFinderInterface; |
14
|
|
|
use yii\db\ConstraintFinderTrait; |
15
|
|
|
use yii\db\DefaultValueConstraint; |
16
|
|
|
use yii\db\ForeignKeyConstraint; |
17
|
|
|
use yii\db\IndexConstraint; |
18
|
|
|
use yii\db\ViewFinderTrait; |
19
|
|
|
use yii\helpers\ArrayHelper; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Schema is the class for retrieving metadata from MS SQL Server databases (version 2008 and above). |
23
|
|
|
* |
24
|
|
|
* @author Timur Ruziev <[email protected]> |
25
|
|
|
* @since 2.0 |
26
|
|
|
*/ |
27
|
|
|
class Schema extends \yii\db\Schema implements ConstraintFinderInterface |
28
|
|
|
{ |
29
|
|
|
use ViewFinderTrait; |
30
|
|
|
use ConstraintFinderTrait; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var string the default schema used for the current session. |
34
|
|
|
*/ |
35
|
|
|
public $defaultSchema = 'dbo'; |
36
|
|
|
/** |
37
|
|
|
* @var array mapping from physical column types (keys) to abstract column types (values) |
38
|
|
|
*/ |
39
|
|
|
public $typeMap = [ |
40
|
|
|
// exact numbers |
41
|
|
|
'bigint' => self::TYPE_BIGINT, |
42
|
|
|
'numeric' => self::TYPE_DECIMAL, |
43
|
|
|
'bit' => self::TYPE_SMALLINT, |
44
|
|
|
'smallint' => self::TYPE_SMALLINT, |
45
|
|
|
'decimal' => self::TYPE_DECIMAL, |
46
|
|
|
'smallmoney' => self::TYPE_MONEY, |
47
|
|
|
'int' => self::TYPE_INTEGER, |
48
|
|
|
'tinyint' => self::TYPE_TINYINT, |
49
|
|
|
'money' => self::TYPE_MONEY, |
50
|
|
|
// approximate numbers |
51
|
|
|
'float' => self::TYPE_FLOAT, |
52
|
|
|
'double' => self::TYPE_DOUBLE, |
53
|
|
|
'real' => self::TYPE_FLOAT, |
54
|
|
|
// date and time |
55
|
|
|
'date' => self::TYPE_DATE, |
56
|
|
|
'datetimeoffset' => self::TYPE_DATETIME, |
57
|
|
|
'datetime2' => self::TYPE_DATETIME, |
58
|
|
|
'smalldatetime' => self::TYPE_DATETIME, |
59
|
|
|
'datetime' => self::TYPE_DATETIME, |
60
|
|
|
'time' => self::TYPE_TIME, |
61
|
|
|
// character strings |
62
|
|
|
'char' => self::TYPE_CHAR, |
63
|
|
|
'varchar' => self::TYPE_STRING, |
64
|
|
|
'text' => self::TYPE_TEXT, |
65
|
|
|
// unicode character strings |
66
|
|
|
'nchar' => self::TYPE_CHAR, |
67
|
|
|
'nvarchar' => self::TYPE_STRING, |
68
|
|
|
'ntext' => self::TYPE_TEXT, |
69
|
|
|
// binary strings |
70
|
|
|
'binary' => self::TYPE_BINARY, |
71
|
|
|
'varbinary' => self::TYPE_BINARY, |
72
|
|
|
'image' => self::TYPE_BINARY, |
73
|
|
|
// other data types |
74
|
|
|
// 'cursor' type cannot be used with tables |
75
|
|
|
'timestamp' => self::TYPE_TIMESTAMP, |
76
|
|
|
'hierarchyid' => self::TYPE_STRING, |
77
|
|
|
'uniqueidentifier' => self::TYPE_STRING, |
78
|
|
|
'sql_variant' => self::TYPE_STRING, |
79
|
|
|
'xml' => self::TYPE_STRING, |
80
|
|
|
'table' => self::TYPE_STRING, |
81
|
|
|
]; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @inheritDoc |
85
|
|
|
*/ |
86
|
|
|
protected $tableQuoteCharacter = ['[', ']']; |
87
|
|
|
/** |
88
|
|
|
* @inheritDoc |
89
|
|
|
*/ |
90
|
|
|
protected $columnQuoteCharacter = ['[', ']']; |
91
|
|
|
|
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Resolves the table name and schema name (if any). |
95
|
|
|
* @param string $name the table name |
96
|
|
|
* @return TableSchema resolved table, schema, etc. names. |
97
|
|
|
*/ |
98
|
|
|
protected function resolveTableName($name) |
99
|
|
|
{ |
100
|
|
|
$resolvedName = new TableSchema(); |
101
|
|
|
$parts = explode('.', str_replace(['[', ']'], '', $name)); |
102
|
|
|
$partCount = count($parts); |
103
|
|
|
if ($partCount === 4) { |
104
|
|
|
// server name, catalog name, schema name and table name passed |
105
|
|
|
$resolvedName->catalogName = $parts[1]; |
106
|
|
|
$resolvedName->schemaName = $parts[2]; |
107
|
|
|
$resolvedName->name = $parts[3]; |
108
|
|
|
$resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name; |
109
|
|
|
} elseif ($partCount === 3) { |
110
|
|
|
// catalog name, schema name and table name passed |
111
|
|
|
$resolvedName->catalogName = $parts[0]; |
112
|
|
|
$resolvedName->schemaName = $parts[1]; |
113
|
|
|
$resolvedName->name = $parts[2]; |
114
|
|
|
$resolvedName->fullName = $resolvedName->catalogName . '.' . $resolvedName->schemaName . '.' . $resolvedName->name; |
115
|
|
|
} elseif ($partCount === 2) { |
116
|
|
|
// only schema name and table name passed |
117
|
|
|
$resolvedName->schemaName = $parts[0]; |
118
|
|
|
$resolvedName->name = $parts[1]; |
119
|
|
|
$resolvedName->fullName = ($resolvedName->schemaName !== $this->defaultSchema ? $resolvedName->schemaName . '.' : '') . $resolvedName->name; |
120
|
|
|
} else { |
121
|
|
|
// only table name passed |
122
|
|
|
$resolvedName->schemaName = $this->defaultSchema; |
123
|
|
|
$resolvedName->fullName = $resolvedName->name = $parts[0]; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
return $resolvedName; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* @inheritDoc |
131
|
|
|
* @see https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-principals-transact-sql |
132
|
|
|
*/ |
133
|
|
|
protected function findSchemaNames() |
134
|
|
|
{ |
135
|
|
|
static $sql = <<<'SQL' |
136
|
|
|
SELECT [s].[name] |
137
|
|
|
FROM [sys].[schemas] AS [s] |
138
|
|
|
INNER JOIN [sys].[database_principals] AS [p] ON [p].[principal_id] = [s].[principal_id] |
139
|
|
|
WHERE [p].[is_fixed_role] = 0 AND [p].[sid] IS NOT NULL |
140
|
|
|
ORDER BY [s].[name] ASC |
141
|
|
|
SQL; |
142
|
|
|
|
143
|
|
|
return $this->db->createCommand($sql)->queryColumn(); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @inheritDoc |
148
|
|
|
*/ |
149
|
|
|
protected function findTableNames($schema = '') |
150
|
|
|
{ |
151
|
|
|
if ($schema === '') { |
152
|
|
|
$schema = $this->defaultSchema; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
$sql = <<<'SQL' |
156
|
|
|
SELECT [t].[table_name] |
157
|
|
|
FROM [INFORMATION_SCHEMA].[TABLES] AS [t] |
158
|
|
|
WHERE [t].[table_schema] = :schema AND [t].[table_type] IN ('BASE TABLE', 'VIEW') |
159
|
|
|
ORDER BY [t].[table_name] |
160
|
|
|
SQL; |
161
|
|
|
|
162
|
|
|
return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @inheritDoc |
167
|
|
|
*/ |
168
|
|
|
protected function loadTableSchema($name) |
169
|
|
|
{ |
170
|
|
|
$table = new TableSchema(); |
171
|
|
|
$this->resolveTableNames($table, $name); |
172
|
|
|
$this->findPrimaryKeys($table); |
173
|
|
|
if ($this->findColumns($table)) { |
174
|
|
|
$this->findForeignKeys($table); |
175
|
|
|
return $table; |
|
|
|
|
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
return null; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* @inheritDoc |
183
|
|
|
*/ |
184
|
|
|
protected function loadTablePrimaryKey($tableName) |
185
|
|
|
{ |
186
|
|
|
return $this->loadTableConstraints($tableName, 'primaryKey'); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* @inheritDoc |
191
|
|
|
*/ |
192
|
|
|
protected function loadTableForeignKeys($tableName) |
193
|
|
|
{ |
194
|
|
|
return $this->loadTableConstraints($tableName, 'foreignKeys'); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @inheritDoc |
199
|
|
|
*/ |
200
|
|
|
protected function loadTableIndexes($tableName) |
201
|
|
|
{ |
202
|
|
|
static $sql = <<<'SQL' |
203
|
|
|
SELECT |
204
|
|
|
[i].[name] AS [name], |
205
|
|
|
[iccol].[name] AS [column_name], |
206
|
|
|
[i].[is_unique] AS [index_is_unique], |
207
|
|
|
[i].[is_primary_key] AS [index_is_primary] |
208
|
|
|
FROM [sys].[indexes] AS [i] |
209
|
|
|
INNER JOIN [sys].[index_columns] AS [ic] |
210
|
|
|
ON [ic].[object_id] = [i].[object_id] AND [ic].[index_id] = [i].[index_id] |
211
|
|
|
INNER JOIN [sys].[columns] AS [iccol] |
212
|
|
|
ON [iccol].[object_id] = [ic].[object_id] AND [iccol].[column_id] = [ic].[column_id] |
213
|
|
|
WHERE [i].[object_id] = OBJECT_ID(:fullName) |
214
|
|
|
ORDER BY [ic].[key_ordinal] ASC |
215
|
|
|
SQL; |
216
|
|
|
|
217
|
|
|
$resolvedName = $this->resolveTableName($tableName); |
218
|
|
|
$indexes = $this->db->createCommand($sql, [ |
219
|
|
|
':fullName' => $resolvedName->fullName, |
220
|
|
|
])->queryAll(); |
221
|
|
|
$indexes = $this->normalizePdoRowKeyCase($indexes, true); |
222
|
|
|
$indexes = ArrayHelper::index($indexes, null, 'name'); |
223
|
|
|
$result = []; |
224
|
|
|
foreach ($indexes as $name => $index) { |
225
|
|
|
$result[] = new IndexConstraint([ |
226
|
|
|
'isPrimary' => (bool) $index[0]['index_is_primary'], |
227
|
|
|
'isUnique' => (bool) $index[0]['index_is_unique'], |
228
|
|
|
'name' => $name, |
229
|
|
|
'columnNames' => ArrayHelper::getColumn($index, 'column_name'), |
230
|
|
|
]); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
return $result; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @inheritDoc |
238
|
|
|
*/ |
239
|
|
|
protected function loadTableUniques($tableName) |
240
|
|
|
{ |
241
|
|
|
return $this->loadTableConstraints($tableName, 'uniques'); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @inheritDoc |
246
|
|
|
*/ |
247
|
|
|
protected function loadTableChecks($tableName) |
248
|
|
|
{ |
249
|
|
|
return $this->loadTableConstraints($tableName, 'checks'); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* @inheritDoc |
254
|
|
|
*/ |
255
|
|
|
protected function loadTableDefaultValues($tableName) |
256
|
|
|
{ |
257
|
|
|
return $this->loadTableConstraints($tableName, 'defaults'); |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* {@inheritdoc} |
262
|
|
|
*/ |
263
|
|
|
public function createSavepoint($name) |
264
|
|
|
{ |
265
|
|
|
$this->db->createCommand("SAVE TRANSACTION $name")->execute(); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* {@inheritdoc} |
270
|
|
|
*/ |
271
|
|
|
public function releaseSavepoint($name) |
272
|
|
|
{ |
273
|
|
|
// does nothing as MSSQL does not support this |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* {@inheritdoc} |
278
|
|
|
*/ |
279
|
|
|
public function rollBackSavepoint($name) |
280
|
|
|
{ |
281
|
|
|
$this->db->createCommand("ROLLBACK TRANSACTION $name")->execute(); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Creates a query builder for the MSSQL database. |
286
|
|
|
* @return QueryBuilder query builder interface. |
287
|
|
|
*/ |
288
|
|
|
public function createQueryBuilder() |
289
|
|
|
{ |
290
|
|
|
return new QueryBuilder($this->db); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Resolves the table name and schema name (if any). |
295
|
|
|
* @param TableSchema $table the table metadata object |
296
|
|
|
* @param string $name the table name |
297
|
|
|
*/ |
298
|
|
|
protected function resolveTableNames($table, $name) |
299
|
|
|
{ |
300
|
|
|
$parts = explode('.', str_replace(['[', ']'], '', $name)); |
301
|
|
|
$partCount = count($parts); |
302
|
|
|
if ($partCount === 4) { |
303
|
|
|
// server name, catalog name, schema name and table name passed |
304
|
|
|
$table->catalogName = $parts[1]; |
305
|
|
|
$table->schemaName = $parts[2]; |
306
|
|
|
$table->name = $parts[3]; |
307
|
|
|
$table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name; |
308
|
|
|
} elseif ($partCount === 3) { |
309
|
|
|
// catalog name, schema name and table name passed |
310
|
|
|
$table->catalogName = $parts[0]; |
311
|
|
|
$table->schemaName = $parts[1]; |
312
|
|
|
$table->name = $parts[2]; |
313
|
|
|
$table->fullName = $table->catalogName . '.' . $table->schemaName . '.' . $table->name; |
314
|
|
|
} elseif ($partCount === 2) { |
315
|
|
|
// only schema name and table name passed |
316
|
|
|
$table->schemaName = $parts[0]; |
317
|
|
|
$table->name = $parts[1]; |
318
|
|
|
$table->fullName = $table->schemaName !== $this->defaultSchema ? $table->schemaName . '.' . $table->name : $table->name; |
319
|
|
|
} else { |
320
|
|
|
// only table name passed |
321
|
|
|
$table->schemaName = $this->defaultSchema; |
322
|
|
|
$table->fullName = $table->name = $parts[0]; |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* Loads the column information into a [[ColumnSchema]] object. |
328
|
|
|
* @param array $info column information |
329
|
|
|
* @return ColumnSchema the column schema object |
330
|
|
|
*/ |
331
|
|
|
protected function loadColumnSchema($info) |
332
|
|
|
{ |
333
|
|
|
$column = $this->createColumnSchema(); |
334
|
|
|
|
335
|
|
|
$column->name = $info['column_name']; |
336
|
|
|
$column->allowNull = $info['is_nullable'] === 'YES'; |
337
|
|
|
$column->dbType = $info['data_type']; |
338
|
|
|
$column->enumValues = []; // mssql has only vague equivalents to enum |
339
|
|
|
$column->isPrimaryKey = null; // primary key will be determined in findColumns() method |
340
|
|
|
$column->autoIncrement = $info['is_identity'] == 1; |
341
|
|
|
$column->unsigned = stripos($column->dbType, 'unsigned') !== false; |
342
|
|
|
$column->comment = $info['comment'] === null ? '' : $info['comment']; |
343
|
|
|
|
344
|
|
|
$column->type = self::TYPE_STRING; |
345
|
|
|
if (preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $column->dbType, $matches)) { |
346
|
|
|
$type = $matches[1]; |
347
|
|
|
if (isset($this->typeMap[$type])) { |
348
|
|
|
$column->type = $this->typeMap[$type]; |
349
|
|
|
} |
350
|
|
|
if (!empty($matches[2])) { |
351
|
|
|
$values = explode(',', $matches[2]); |
352
|
|
|
$column->size = $column->precision = (int) $values[0]; |
353
|
|
|
if (isset($values[1])) { |
354
|
|
|
$column->scale = (int) $values[1]; |
355
|
|
|
} |
356
|
|
|
if ($column->size === 1 && ($type === 'tinyint' || $type === 'bit')) { |
357
|
|
|
$column->type = 'boolean'; |
358
|
|
|
} elseif ($type === 'bit') { |
359
|
|
|
if ($column->size > 32) { |
360
|
|
|
$column->type = 'bigint'; |
361
|
|
|
} elseif ($column->size === 32) { |
362
|
|
|
$column->type = 'integer'; |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
$column->phpType = $this->getColumnPhpType($column); |
369
|
|
|
|
370
|
|
|
if ($info['column_default'] === '(NULL)') { |
371
|
|
|
$info['column_default'] = null; |
372
|
|
|
} |
373
|
|
|
if (!$column->isPrimaryKey && ($column->type !== 'timestamp' || $info['column_default'] !== 'CURRENT_TIMESTAMP')) { |
374
|
|
|
$column->defaultValue = $column->phpTypecast($info['column_default']); |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
return $column; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* Collects the metadata of table columns. |
382
|
|
|
* @param TableSchema $table the table metadata |
383
|
|
|
* @return bool whether the table exists in the database |
384
|
|
|
*/ |
385
|
|
|
protected function findColumns($table) |
386
|
|
|
{ |
387
|
|
|
$columnsTableName = 'INFORMATION_SCHEMA.COLUMNS'; |
388
|
|
|
$whereSql = "[t1].[table_name] = '{$table->name}'"; |
389
|
|
|
if ($table->catalogName !== null) { |
390
|
|
|
$columnsTableName = "{$table->catalogName}.{$columnsTableName}"; |
391
|
|
|
$whereSql .= " AND [t1].[table_catalog] = '{$table->catalogName}'"; |
392
|
|
|
} |
393
|
|
|
if ($table->schemaName !== null) { |
394
|
|
|
$whereSql .= " AND [t1].[table_schema] = '{$table->schemaName}'"; |
395
|
|
|
} |
396
|
|
|
$columnsTableName = $this->quoteTableName($columnsTableName); |
397
|
|
|
|
398
|
|
|
$sql = <<<SQL |
399
|
|
|
SELECT |
400
|
|
|
[t1].[column_name], |
401
|
|
|
[t1].[is_nullable], |
402
|
|
|
[t1].[data_type], |
403
|
|
|
[t1].[column_default], |
404
|
|
|
COLUMNPROPERTY(OBJECT_ID([t1].[table_schema] + '.' + [t1].[table_name]), [t1].[column_name], 'IsIdentity') AS is_identity, |
405
|
|
|
( |
406
|
|
|
SELECT CONVERT(VARCHAR, [t2].[value]) |
407
|
|
|
FROM [sys].[extended_properties] AS [t2] |
408
|
|
|
WHERE |
409
|
|
|
[t2].[class] = 1 AND |
410
|
|
|
[t2].[class_desc] = 'OBJECT_OR_COLUMN' AND |
411
|
|
|
[t2].[name] = 'MS_Description' AND |
412
|
|
|
[t2].[major_id] = OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[table_name]) AND |
413
|
|
|
[t2].[minor_id] = COLUMNPROPERTY(OBJECT_ID([t1].[TABLE_SCHEMA] + '.' + [t1].[TABLE_NAME]), [t1].[COLUMN_NAME], 'ColumnID') |
414
|
|
|
) as comment |
415
|
|
|
FROM {$columnsTableName} AS [t1] |
416
|
|
|
WHERE {$whereSql} |
417
|
|
|
SQL; |
418
|
|
|
|
419
|
|
|
try { |
420
|
|
|
$columns = $this->db->createCommand($sql)->queryAll(); |
421
|
|
|
if (empty($columns)) { |
422
|
|
|
return false; |
423
|
|
|
} |
424
|
|
|
} catch (\Exception $e) { |
425
|
|
|
return false; |
426
|
|
|
} |
427
|
|
|
foreach ($columns as $column) { |
428
|
|
|
$column = $this->loadColumnSchema($column); |
429
|
|
|
foreach ($table->primaryKey as $primaryKey) { |
430
|
|
|
if (strcasecmp($column->name, $primaryKey) === 0) { |
431
|
|
|
$column->isPrimaryKey = true; |
432
|
|
|
break; |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
if ($column->isPrimaryKey && $column->autoIncrement) { |
436
|
|
|
$table->sequenceName = ''; |
437
|
|
|
} |
438
|
|
|
$table->columns[$column->name] = $column; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
return true; |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
/** |
445
|
|
|
* Collects the constraint details for the given table and constraint type. |
446
|
|
|
* @param TableSchema $table |
447
|
|
|
* @param string $type either PRIMARY KEY or UNIQUE |
448
|
|
|
* @return array each entry contains index_name and field_name |
449
|
|
|
* @since 2.0.4 |
450
|
|
|
*/ |
451
|
|
|
protected function findTableConstraints($table, $type) |
452
|
|
|
{ |
453
|
|
|
$keyColumnUsageTableName = 'INFORMATION_SCHEMA.KEY_COLUMN_USAGE'; |
454
|
|
|
$tableConstraintsTableName = 'INFORMATION_SCHEMA.TABLE_CONSTRAINTS'; |
455
|
|
|
if ($table->catalogName !== null) { |
456
|
|
|
$keyColumnUsageTableName = $table->catalogName . '.' . $keyColumnUsageTableName; |
457
|
|
|
$tableConstraintsTableName = $table->catalogName . '.' . $tableConstraintsTableName; |
458
|
|
|
} |
459
|
|
|
$keyColumnUsageTableName = $this->quoteTableName($keyColumnUsageTableName); |
460
|
|
|
$tableConstraintsTableName = $this->quoteTableName($tableConstraintsTableName); |
461
|
|
|
|
462
|
|
|
$sql = <<<SQL |
463
|
|
|
SELECT |
464
|
|
|
[kcu].[constraint_name] AS [index_name], |
465
|
|
|
[kcu].[column_name] AS [field_name] |
466
|
|
|
FROM {$keyColumnUsageTableName} AS [kcu] |
467
|
|
|
LEFT JOIN {$tableConstraintsTableName} AS [tc] ON |
468
|
|
|
[kcu].[table_schema] = [tc].[table_schema] AND |
469
|
|
|
[kcu].[table_name] = [tc].[table_name] AND |
470
|
|
|
[kcu].[constraint_name] = [tc].[constraint_name] |
471
|
|
|
WHERE |
472
|
|
|
[tc].[constraint_type] = :type AND |
473
|
|
|
[kcu].[table_name] = :tableName AND |
474
|
|
|
[kcu].[table_schema] = :schemaName |
475
|
|
|
SQL; |
476
|
|
|
|
477
|
|
|
return $this->db |
478
|
|
|
->createCommand($sql, [ |
479
|
|
|
':tableName' => $table->name, |
480
|
|
|
':schemaName' => $table->schemaName, |
481
|
|
|
':type' => $type, |
482
|
|
|
]) |
483
|
|
|
->queryAll(); |
484
|
|
|
} |
485
|
|
|
|
486
|
|
|
/** |
487
|
|
|
* Collects the primary key column details for the given table. |
488
|
|
|
* @param TableSchema $table the table metadata |
489
|
|
|
*/ |
490
|
|
|
protected function findPrimaryKeys($table) |
491
|
|
|
{ |
492
|
|
|
$result = []; |
493
|
|
|
foreach ($this->findTableConstraints($table, 'PRIMARY KEY') as $row) { |
494
|
|
|
$result[] = $row['field_name']; |
495
|
|
|
} |
496
|
|
|
$table->primaryKey = $result; |
497
|
|
|
} |
498
|
|
|
|
499
|
|
|
/** |
500
|
|
|
* Collects the foreign key column details for the given table. |
501
|
|
|
* @param TableSchema $table the table metadata |
502
|
|
|
*/ |
503
|
|
|
protected function findForeignKeys($table) |
504
|
|
|
{ |
505
|
|
|
$object = $table->name; |
506
|
|
|
if ($table->schemaName !== null) { |
507
|
|
|
$object = $table->schemaName . '.' . $object; |
508
|
|
|
} |
509
|
|
|
if ($table->catalogName !== null) { |
510
|
|
|
$object = $table->catalogName . '.' . $object; |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
// please refer to the following page for more details: |
514
|
|
|
// http://msdn2.microsoft.com/en-us/library/aa175805(SQL.80).aspx |
515
|
|
|
$sql = <<<'SQL' |
516
|
|
|
SELECT |
517
|
|
|
[fk].[name] AS [fk_name], |
518
|
|
|
[cp].[name] AS [fk_column_name], |
519
|
|
|
OBJECT_NAME([fk].[referenced_object_id]) AS [uq_table_name], |
520
|
|
|
[cr].[name] AS [uq_column_name] |
521
|
|
|
FROM |
522
|
|
|
[sys].[foreign_keys] AS [fk] |
523
|
|
|
INNER JOIN [sys].[foreign_key_columns] AS [fkc] ON |
524
|
|
|
[fk].[object_id] = [fkc].[constraint_object_id] |
525
|
|
|
INNER JOIN [sys].[columns] AS [cp] ON |
526
|
|
|
[fk].[parent_object_id] = [cp].[object_id] AND |
527
|
|
|
[fkc].[parent_column_id] = [cp].[column_id] |
528
|
|
|
INNER JOIN [sys].[columns] AS [cr] ON |
529
|
|
|
[fk].[referenced_object_id] = [cr].[object_id] AND |
530
|
|
|
[fkc].[referenced_column_id] = [cr].[column_id] |
531
|
|
|
WHERE |
532
|
|
|
[fk].[parent_object_id] = OBJECT_ID(:object) |
533
|
|
|
SQL; |
534
|
|
|
|
535
|
|
|
$rows = $this->db->createCommand($sql, [ |
536
|
|
|
':object' => $object, |
537
|
|
|
])->queryAll(); |
538
|
|
|
|
539
|
|
|
$table->foreignKeys = []; |
540
|
|
|
foreach ($rows as $row) { |
541
|
|
|
$table->foreignKeys[$row['fk_name']] = [$row['uq_table_name'], $row['fk_column_name'] => $row['uq_column_name']]; |
542
|
|
|
} |
543
|
|
|
} |
544
|
|
|
|
545
|
|
|
/** |
546
|
|
|
* {@inheritdoc} |
547
|
|
|
*/ |
548
|
|
|
protected function findViewNames($schema = '') |
549
|
|
|
{ |
550
|
|
|
if ($schema === '') { |
551
|
|
|
$schema = $this->defaultSchema; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
$sql = <<<'SQL' |
555
|
|
|
SELECT [t].[table_name] |
556
|
|
|
FROM [INFORMATION_SCHEMA].[TABLES] AS [t] |
557
|
|
|
WHERE [t].[table_schema] = :schema AND [t].[table_type] = 'VIEW' |
558
|
|
|
ORDER BY [t].[table_name] |
559
|
|
|
SQL; |
560
|
|
|
|
561
|
|
|
return $this->db->createCommand($sql, [':schema' => $schema])->queryColumn(); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Returns all unique indexes for the given table. |
566
|
|
|
* |
567
|
|
|
* Each array element is of the following structure: |
568
|
|
|
* |
569
|
|
|
* ```php |
570
|
|
|
* [ |
571
|
|
|
* 'IndexName1' => ['col1' [, ...]], |
572
|
|
|
* 'IndexName2' => ['col2' [, ...]], |
573
|
|
|
* ] |
574
|
|
|
* ``` |
575
|
|
|
* |
576
|
|
|
* @param TableSchema $table the table metadata |
577
|
|
|
* @return array all unique indexes for the given table. |
578
|
|
|
* @since 2.0.4 |
579
|
|
|
*/ |
580
|
|
|
public function findUniqueIndexes($table) |
581
|
|
|
{ |
582
|
|
|
$result = []; |
583
|
|
|
foreach ($this->findTableConstraints($table, 'UNIQUE') as $row) { |
584
|
|
|
$result[$row['index_name']][] = $row['field_name']; |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
return $result; |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* Loads multiple types of constraints and returns the specified ones. |
592
|
|
|
* @param string $tableName table name. |
593
|
|
|
* @param string $returnType return type: |
594
|
|
|
* - primaryKey |
595
|
|
|
* - foreignKeys |
596
|
|
|
* - uniques |
597
|
|
|
* - checks |
598
|
|
|
* - defaults |
599
|
|
|
* @return mixed constraints. |
600
|
|
|
*/ |
601
|
|
|
private function loadTableConstraints($tableName, $returnType) |
602
|
|
|
{ |
603
|
|
|
static $sql = <<<'SQL' |
604
|
|
|
SELECT |
605
|
|
|
[o].[name] AS [name], |
606
|
|
|
COALESCE([ccol].[name], [dcol].[name], [fccol].[name], [kiccol].[name]) AS [column_name], |
607
|
|
|
RTRIM([o].[type]) AS [type], |
608
|
|
|
OBJECT_SCHEMA_NAME([f].[referenced_object_id]) AS [foreign_table_schema], |
609
|
|
|
OBJECT_NAME([f].[referenced_object_id]) AS [foreign_table_name], |
610
|
|
|
[ffccol].[name] AS [foreign_column_name], |
611
|
|
|
[f].[update_referential_action_desc] AS [on_update], |
612
|
|
|
[f].[delete_referential_action_desc] AS [on_delete], |
613
|
|
|
[c].[definition] AS [check_expr], |
614
|
|
|
[d].[definition] AS [default_expr] |
615
|
|
|
FROM (SELECT OBJECT_ID(:fullName) AS [object_id]) AS [t] |
616
|
|
|
INNER JOIN [sys].[objects] AS [o] |
617
|
|
|
ON [o].[parent_object_id] = [t].[object_id] AND [o].[type] IN ('PK', 'UQ', 'C', 'D', 'F') |
618
|
|
|
LEFT JOIN [sys].[check_constraints] AS [c] |
619
|
|
|
ON [c].[object_id] = [o].[object_id] |
620
|
|
|
LEFT JOIN [sys].[columns] AS [ccol] |
621
|
|
|
ON [ccol].[object_id] = [c].[parent_object_id] AND [ccol].[column_id] = [c].[parent_column_id] |
622
|
|
|
LEFT JOIN [sys].[default_constraints] AS [d] |
623
|
|
|
ON [d].[object_id] = [o].[object_id] |
624
|
|
|
LEFT JOIN [sys].[columns] AS [dcol] |
625
|
|
|
ON [dcol].[object_id] = [d].[parent_object_id] AND [dcol].[column_id] = [d].[parent_column_id] |
626
|
|
|
LEFT JOIN [sys].[key_constraints] AS [k] |
627
|
|
|
ON [k].[object_id] = [o].[object_id] |
628
|
|
|
LEFT JOIN [sys].[index_columns] AS [kic] |
629
|
|
|
ON [kic].[object_id] = [k].[parent_object_id] AND [kic].[index_id] = [k].[unique_index_id] |
630
|
|
|
LEFT JOIN [sys].[columns] AS [kiccol] |
631
|
|
|
ON [kiccol].[object_id] = [kic].[object_id] AND [kiccol].[column_id] = [kic].[column_id] |
632
|
|
|
LEFT JOIN [sys].[foreign_keys] AS [f] |
633
|
|
|
ON [f].[object_id] = [o].[object_id] |
634
|
|
|
LEFT JOIN [sys].[foreign_key_columns] AS [fc] |
635
|
|
|
ON [fc].[constraint_object_id] = [o].[object_id] |
636
|
|
|
LEFT JOIN [sys].[columns] AS [fccol] |
637
|
|
|
ON [fccol].[object_id] = [fc].[parent_object_id] AND [fccol].[column_id] = [fc].[parent_column_id] |
638
|
|
|
LEFT JOIN [sys].[columns] AS [ffccol] |
639
|
|
|
ON [ffccol].[object_id] = [fc].[referenced_object_id] AND [ffccol].[column_id] = [fc].[referenced_column_id] |
640
|
|
|
ORDER BY [kic].[key_ordinal] ASC, [fc].[constraint_column_id] ASC |
641
|
|
|
SQL; |
642
|
|
|
|
643
|
|
|
$resolvedName = $this->resolveTableName($tableName); |
644
|
|
|
$constraints = $this->db->createCommand($sql, [ |
645
|
|
|
':fullName' => $resolvedName->fullName, |
646
|
|
|
])->queryAll(); |
647
|
|
|
$constraints = $this->normalizePdoRowKeyCase($constraints, true); |
648
|
|
|
$constraints = ArrayHelper::index($constraints, null, ['type', 'name']); |
649
|
|
|
$result = [ |
650
|
|
|
'primaryKey' => null, |
651
|
|
|
'foreignKeys' => [], |
652
|
|
|
'uniques' => [], |
653
|
|
|
'checks' => [], |
654
|
|
|
'defaults' => [], |
655
|
|
|
]; |
656
|
|
|
foreach ($constraints as $type => $names) { |
657
|
|
|
foreach ($names as $name => $constraint) { |
658
|
|
|
switch ($type) { |
659
|
|
|
case 'PK': |
660
|
|
|
$result['primaryKey'] = new Constraint([ |
661
|
|
|
'name' => $name, |
662
|
|
|
'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), |
663
|
|
|
]); |
664
|
|
|
break; |
665
|
|
|
case 'F': |
666
|
|
|
$result['foreignKeys'][] = new ForeignKeyConstraint([ |
667
|
|
|
'name' => $name, |
668
|
|
|
'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), |
669
|
|
|
'foreignSchemaName' => $constraint[0]['foreign_table_schema'], |
670
|
|
|
'foreignTableName' => $constraint[0]['foreign_table_name'], |
671
|
|
|
'foreignColumnNames' => ArrayHelper::getColumn($constraint, 'foreign_column_name'), |
672
|
|
|
'onDelete' => str_replace('_', '', $constraint[0]['on_delete']), |
673
|
|
|
'onUpdate' => str_replace('_', '', $constraint[0]['on_update']), |
674
|
|
|
]); |
675
|
|
|
break; |
676
|
|
|
case 'UQ': |
677
|
|
|
$result['uniques'][] = new Constraint([ |
678
|
|
|
'name' => $name, |
679
|
|
|
'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), |
680
|
|
|
]); |
681
|
|
|
break; |
682
|
|
|
case 'C': |
683
|
|
|
$result['checks'][] = new CheckConstraint([ |
684
|
|
|
'name' => $name, |
685
|
|
|
'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), |
686
|
|
|
'expression' => $constraint[0]['check_expr'], |
687
|
|
|
]); |
688
|
|
|
break; |
689
|
|
|
case 'D': |
690
|
|
|
$result['defaults'][] = new DefaultValueConstraint([ |
691
|
|
|
'name' => $name, |
692
|
|
|
'columnNames' => ArrayHelper::getColumn($constraint, 'column_name'), |
693
|
|
|
'value' => $constraint[0]['default_expr'], |
694
|
|
|
]); |
695
|
|
|
break; |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
} |
699
|
|
|
foreach ($result as $type => $data) { |
700
|
|
|
$this->setTableMetadata($tableName, $type, $data); |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
return $result[$returnType]; |
704
|
|
|
} |
705
|
|
|
} |
706
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.