1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace Yiisoft\Db\Mysql; |
||||
6 | |||||
7 | use JsonException; |
||||
8 | use Throwable; |
||||
9 | use Yiisoft\Db\Constraint\Constraint; |
||||
10 | use Yiisoft\Db\Constraint\ForeignKeyConstraint; |
||||
11 | use Yiisoft\Db\Constraint\IndexConstraint; |
||||
12 | use Yiisoft\Db\Driver\Pdo\AbstractPdoSchema; |
||||
13 | use Yiisoft\Db\Exception\Exception; |
||||
14 | use Yiisoft\Db\Exception\InvalidConfigException; |
||||
15 | use Yiisoft\Db\Exception\NotSupportedException; |
||||
16 | use Yiisoft\Db\Expression\Expression; |
||||
17 | use Yiisoft\Db\Helper\DbArrayHelper; |
||||
18 | use Yiisoft\Db\Schema\Builder\ColumnInterface; |
||||
19 | use Yiisoft\Db\Schema\ColumnSchemaInterface; |
||||
20 | use Yiisoft\Db\Schema\TableSchemaInterface; |
||||
21 | |||||
22 | use function array_change_key_case; |
||||
23 | use function array_map; |
||||
24 | use function array_merge; |
||||
25 | use function array_values; |
||||
26 | use function bindec; |
||||
27 | use function explode; |
||||
28 | use function in_array; |
||||
29 | use function is_string; |
||||
30 | use function ksort; |
||||
31 | use function md5; |
||||
32 | use function preg_match_all; |
||||
33 | use function preg_match; |
||||
34 | use function serialize; |
||||
35 | use function stripos; |
||||
36 | use function strtolower; |
||||
37 | use function trim; |
||||
38 | |||||
39 | /** |
||||
40 | * Implements MySQL, MariaDB specific schema, supporting MySQL Server 5.7, MariaDB Server 10.4 and higher. |
||||
41 | * |
||||
42 | * @psalm-type ColumnArray = array{ |
||||
43 | * table_schema: string, |
||||
44 | * table_name: string, |
||||
45 | * column_name: string, |
||||
46 | * data_type: string, |
||||
47 | * type_type: string|null, |
||||
48 | * character_maximum_length: int, |
||||
49 | * column_comment: string|null, |
||||
50 | * modifier: int, |
||||
51 | * is_nullable: bool, |
||||
52 | * column_default: mixed, |
||||
53 | * is_autoinc: bool, |
||||
54 | * sequence_name: string|null, |
||||
55 | * enum_values: array<array-key, float|int|string>|string|null, |
||||
56 | * numeric_precision: int|null, |
||||
57 | * numeric_scale: int|null, |
||||
58 | * size: string|null, |
||||
59 | * is_pkey: bool|null, |
||||
60 | * dimension: int |
||||
61 | * } |
||||
62 | * @psalm-type ColumnInfoArray = array{ |
||||
63 | * field: string, |
||||
64 | * type: string, |
||||
65 | * collation: string|null, |
||||
66 | * null: string, |
||||
67 | * key: string, |
||||
68 | * default: string|null, |
||||
69 | * extra: string, |
||||
70 | * extra_default_value: string|null, |
||||
71 | * privileges: string, |
||||
72 | * comment: string |
||||
73 | * } |
||||
74 | * @psalm-type RowConstraint = array{ |
||||
75 | * constraint_name: string, |
||||
76 | * column_name: string, |
||||
77 | * referenced_table_name: string, |
||||
78 | * referenced_column_name: string |
||||
79 | * } |
||||
80 | * @psalm-type ConstraintArray = array< |
||||
81 | * array-key, |
||||
82 | * array { |
||||
83 | * name: string, |
||||
84 | * column_name: string, |
||||
85 | * type: string, |
||||
86 | * foreign_table_schema: string|null, |
||||
87 | * foreign_table_name: string|null, |
||||
88 | * foreign_column_name: string|null, |
||||
89 | * on_update: string, |
||||
90 | * on_delete: string, |
||||
91 | * check_expr: string |
||||
92 | * } |
||||
93 | * > |
||||
94 | */ |
||||
95 | final class Schema extends AbstractPdoSchema |
||||
96 | { |
||||
97 | /** |
||||
98 | * Mapping from physical column types (keys) to abstract column types (values). |
||||
99 | * |
||||
100 | * @var string[] |
||||
101 | */ |
||||
102 | private const TYPE_MAP = [ |
||||
103 | 'tinyint' => self::TYPE_TINYINT, |
||||
104 | 'bit' => self::TYPE_INTEGER, |
||||
105 | 'smallint' => self::TYPE_SMALLINT, |
||||
106 | 'mediumint' => self::TYPE_INTEGER, |
||||
107 | 'int' => self::TYPE_INTEGER, |
||||
108 | 'integer' => self::TYPE_INTEGER, |
||||
109 | 'bigint' => self::TYPE_BIGINT, |
||||
110 | 'float' => self::TYPE_FLOAT, |
||||
111 | 'double' => self::TYPE_DOUBLE, |
||||
112 | 'real' => self::TYPE_FLOAT, |
||||
113 | 'decimal' => self::TYPE_DECIMAL, |
||||
114 | 'numeric' => self::TYPE_DECIMAL, |
||||
115 | 'tinytext' => self::TYPE_TEXT, |
||||
116 | 'mediumtext' => self::TYPE_TEXT, |
||||
117 | 'longtext' => self::TYPE_TEXT, |
||||
118 | 'longblob' => self::TYPE_BINARY, |
||||
119 | 'blob' => self::TYPE_BINARY, |
||||
120 | 'text' => self::TYPE_TEXT, |
||||
121 | 'varchar' => self::TYPE_STRING, |
||||
122 | 'string' => self::TYPE_STRING, |
||||
123 | 'char' => self::TYPE_CHAR, |
||||
124 | 'datetime' => self::TYPE_DATETIME, |
||||
125 | 'year' => self::TYPE_DATE, |
||||
126 | 'date' => self::TYPE_DATE, |
||||
127 | 'time' => self::TYPE_TIME, |
||||
128 | 'timestamp' => self::TYPE_TIMESTAMP, |
||||
129 | 'enum' => self::TYPE_STRING, |
||||
130 | 'varbinary' => self::TYPE_BINARY, |
||||
131 | 'json' => self::TYPE_JSON, |
||||
132 | ]; |
||||
133 | |||||
134 | 16 | public function createColumn(string $type, array|int|string $length = null): ColumnInterface |
|||
135 | { |
||||
136 | 16 | return new Column($type, $length); |
|||
137 | } |
||||
138 | |||||
139 | /** |
||||
140 | * Returns all unique indexes for the given table. |
||||
141 | * |
||||
142 | * Each array element is of the following structure: |
||||
143 | * |
||||
144 | * ```php |
||||
145 | * [ |
||||
146 | * 'IndexName1' => ['col1' [, ...]], |
||||
147 | * 'IndexName2' => ['col2' [, ...]], |
||||
148 | * ] |
||||
149 | * ``` |
||||
150 | * |
||||
151 | * @param TableSchemaInterface $table The table metadata. |
||||
152 | * |
||||
153 | * @throws Exception |
||||
154 | * @throws InvalidConfigException |
||||
155 | * @throws Throwable |
||||
156 | * |
||||
157 | * @return array All unique indexes for the given table. |
||||
158 | */ |
||||
159 | 1 | public function findUniqueIndexes(TableSchemaInterface $table): array |
|||
160 | { |
||||
161 | 1 | $sql = $this->getCreateTableSql($table); |
|||
162 | 1 | $uniqueIndexes = []; |
|||
163 | 1 | $regexp = '/UNIQUE KEY\s+[`"](.+)[`"]\s*\(([`"].+[`"])+\)/mi'; |
|||
164 | |||||
165 | 1 | if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER) > 0) { |
|||
166 | 1 | foreach ($matches as $match) { |
|||
167 | 1 | $indexName = $match[1]; |
|||
168 | 1 | $indexColumns = array_map('trim', preg_split('/[`"],[`"]/', trim($match[2], '`"'))); |
|||
169 | 1 | $uniqueIndexes[$indexName] = $indexColumns; |
|||
170 | } |
||||
171 | } |
||||
172 | |||||
173 | 1 | ksort($uniqueIndexes); |
|||
174 | |||||
175 | 1 | return $uniqueIndexes; |
|||
176 | } |
||||
177 | |||||
178 | /** |
||||
179 | * Collects the metadata of table columns. |
||||
180 | * |
||||
181 | * @param TableSchemaInterface $table The table metadata. |
||||
182 | * |
||||
183 | * @throws Exception |
||||
184 | * @throws Throwable If DB query fails. |
||||
185 | * |
||||
186 | * @return bool Whether the table exists in the database. |
||||
187 | */ |
||||
188 | 178 | protected function findColumns(TableSchemaInterface $table): bool |
|||
189 | { |
||||
190 | 178 | $tableName = $table->getFullName() ?? ''; |
|||
191 | 178 | $sql = 'SHOW FULL COLUMNS FROM ' . $this->db->getQuoter()->quoteTableName($tableName); |
|||
192 | |||||
193 | try { |
||||
194 | 178 | $columns = $this->db->createCommand($sql)->queryAll(); |
|||
195 | // Chapter 1: crutches for MariaDB. {@see https://github.com/yiisoft/yii2/issues/19747} |
||||
196 | 160 | $columnsExtra = []; |
|||
197 | 160 | if (str_contains($this->db->getServerVersion(), 'MariaDB')) { |
|||
198 | /** @psalm-var array[] $columnsExtra */ |
||||
199 | $columnsExtra = $this->db->createCommand( |
||||
200 | <<<SQL |
||||
201 | SELECT `COLUMN_NAME` as name,`COLUMN_DEFAULT` as default_value |
||||
202 | FROM INFORMATION_SCHEMA.COLUMNS |
||||
203 | WHERE TABLE_SCHEMA = COALESCE(:schemaName, DATABASE()) AND TABLE_NAME = :tableName |
||||
204 | SQL , |
||||
205 | [ |
||||
206 | ':schemaName' => $table->getSchemaName(), |
||||
207 | ':tableName' => $table->getName(), |
||||
208 | ] |
||||
209 | )->queryAll(); |
||||
210 | /** @psalm-var string[] $cols */ |
||||
211 | 160 | foreach ($columnsExtra as $cols) { |
|||
212 | $columnsExtra[$cols['name']] = $cols['default_value']; |
||||
213 | } |
||||
214 | } |
||||
215 | 37 | } catch (Exception $e) { |
|||
216 | 37 | $previous = $e->getPrevious(); |
|||
217 | |||||
218 | 37 | if ($previous && str_contains($previous->getMessage(), 'SQLSTATE[42S02')) { |
|||
219 | /** |
||||
220 | * The table doesn't exist. |
||||
221 | * |
||||
222 | * @link https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html#error_er_bad_table_error |
||||
223 | */ |
||||
224 | 37 | return false; |
|||
225 | } |
||||
226 | |||||
227 | throw $e; |
||||
228 | } |
||||
229 | |||||
230 | 160 | $jsonColumns = $this->getJsonColumns($table); |
|||
231 | |||||
232 | /** @psalm-var ColumnInfoArray $info */ |
||||
233 | 160 | foreach ($columns as $info) { |
|||
234 | 160 | $info = array_change_key_case($info); |
|||
235 | |||||
236 | 160 | $info['extra_default_value'] = $columnsExtra[(string) $info['field']] ?? ''; |
|||
237 | |||||
238 | 160 | if (in_array($info['field'], $jsonColumns, true)) { |
|||
239 | $info['type'] = self::TYPE_JSON; |
||||
240 | } |
||||
241 | |||||
242 | /** @psalm-var ColumnInfoArray $info */ |
||||
243 | 160 | $column = $this->loadColumnSchema($info); |
|||
244 | 160 | $table->column($column->getName(), $column); |
|||
245 | |||||
246 | 160 | if ($column->isPrimaryKey()) { |
|||
247 | 93 | $table->primaryKey($column->getName()); |
|||
248 | 93 | if ($column->isAutoIncrement()) { |
|||
249 | 74 | $table->sequenceName(''); |
|||
250 | } |
||||
251 | } |
||||
252 | } |
||||
253 | |||||
254 | 160 | return true; |
|||
255 | } |
||||
256 | |||||
257 | /** |
||||
258 | * Collects the foreign key column details for the given table. |
||||
259 | * |
||||
260 | * @param TableSchemaInterface $table The table metadata. |
||||
261 | * |
||||
262 | * @throws Exception |
||||
263 | * @throws InvalidConfigException |
||||
264 | * @throws Throwable |
||||
265 | */ |
||||
266 | 160 | protected function findConstraints(TableSchemaInterface $table): void |
|||
267 | { |
||||
268 | 160 | $sql = <<<SQL |
|||
269 | SELECT |
||||
270 | `kcu`.`CONSTRAINT_NAME` AS `constraint_name`, |
||||
271 | `kcu`.`COLUMN_NAME` AS `column_name`, |
||||
272 | `kcu`.`REFERENCED_TABLE_NAME` AS `referenced_table_name`, |
||||
273 | `kcu`.`REFERENCED_COLUMN_NAME` AS `referenced_column_name` |
||||
274 | FROM `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` |
||||
275 | JOIN `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` ON |
||||
276 | ( |
||||
277 | `kcu`.`CONSTRAINT_CATALOG` = `rc`.`CONSTRAINT_CATALOG` OR |
||||
278 | ( |
||||
279 | `kcu`.`CONSTRAINT_CATALOG` IS NULL AND |
||||
280 | `rc`.`CONSTRAINT_CATALOG` IS NULL |
||||
281 | ) |
||||
282 | ) AND |
||||
283 | `kcu`.`CONSTRAINT_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND |
||||
284 | `kcu`.`CONSTRAINT_NAME` = `rc`.`CONSTRAINT_NAME` AND |
||||
285 | `kcu`.`TABLE_SCHEMA` = `rc`.`CONSTRAINT_SCHEMA` AND |
||||
286 | `kcu`.`TABLE_NAME` = `rc`.`TABLE_NAME` |
||||
287 | WHERE `rc`.`CONSTRAINT_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND `rc`.`TABLE_NAME` = :tableName |
||||
288 | 160 | SQL; |
|||
289 | |||||
290 | 160 | $constraints = []; |
|||
291 | 160 | $rows = $this->db->createCommand($sql, [ |
|||
292 | 160 | ':schemaName' => $table->getSchemaName(), |
|||
293 | 160 | ':tableName' => $table->getName(), |
|||
294 | 160 | ])->queryAll(); |
|||
295 | |||||
296 | /** @psalm-var RowConstraint $row */ |
||||
297 | 160 | foreach ($rows as $row) { |
|||
298 | 38 | $constraints[$row['constraint_name']]['referenced_table_name'] = $row['referenced_table_name']; |
|||
299 | 38 | $constraints[$row['constraint_name']]['columns'][$row['column_name']] = $row['referenced_column_name']; |
|||
300 | } |
||||
301 | |||||
302 | 160 | $table->foreignKeys([]); |
|||
303 | |||||
304 | /** |
||||
305 | * @psalm-var array{referenced_table_name: string, columns: array} $constraint |
||||
306 | */ |
||||
307 | 160 | foreach ($constraints as $name => $constraint) { |
|||
308 | 38 | $table->foreignKey( |
|||
309 | 38 | $name, |
|||
310 | 38 | array_merge( |
|||
311 | 38 | [$constraint['referenced_table_name']], |
|||
312 | 38 | $constraint['columns'] |
|||
313 | 38 | ), |
|||
314 | 38 | ); |
|||
315 | } |
||||
316 | } |
||||
317 | |||||
318 | /** |
||||
319 | * @throws Exception |
||||
320 | * @throws InvalidConfigException |
||||
321 | * @throws Throwable |
||||
322 | */ |
||||
323 | 1 | protected function findSchemaNames(): array |
|||
324 | { |
||||
325 | 1 | $sql = <<<SQL |
|||
326 | SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') |
||||
327 | 1 | SQL; |
|||
328 | |||||
329 | 1 | return $this->db->createCommand($sql)->queryColumn(); |
|||
330 | } |
||||
331 | |||||
332 | /** |
||||
333 | * @throws Exception |
||||
334 | * @throws InvalidConfigException |
||||
335 | * @throws Throwable |
||||
336 | */ |
||||
337 | 178 | protected function findTableComment(TableSchemaInterface $tableSchema): void |
|||
338 | { |
||||
339 | 178 | $sql = <<<SQL |
|||
340 | SELECT `TABLE_COMMENT` |
||||
341 | FROM `INFORMATION_SCHEMA`.`TABLES` |
||||
342 | WHERE |
||||
343 | `TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND |
||||
344 | `TABLE_NAME` = :tableName; |
||||
345 | 178 | SQL; |
|||
346 | |||||
347 | 178 | $comment = $this->db->createCommand($sql, [ |
|||
348 | 178 | ':schemaName' => $tableSchema->getSchemaName(), |
|||
349 | 178 | ':tableName' => $tableSchema->getName(), |
|||
350 | 178 | ])->queryScalar(); |
|||
351 | |||||
352 | 178 | $tableSchema->comment(is_string($comment) ? $comment : null); |
|||
353 | } |
||||
354 | |||||
355 | /** |
||||
356 | * Returns all table names in the database. |
||||
357 | * |
||||
358 | * This method should be overridden by child classes to support this feature because the default implementation |
||||
359 | * simply throws an exception. |
||||
360 | * |
||||
361 | * @param string $schema The schema of the tables. |
||||
362 | * Defaults to empty string, meaning the current or default schema. |
||||
363 | * |
||||
364 | * @throws Exception |
||||
365 | * @throws InvalidConfigException |
||||
366 | * @throws Throwable |
||||
367 | * |
||||
368 | * @return array All tables name in the database. The names have NO schema name prefix. |
||||
369 | */ |
||||
370 | 12 | protected function findTableNames(string $schema = ''): array |
|||
371 | { |
||||
372 | 12 | $sql = 'SHOW TABLES'; |
|||
373 | |||||
374 | 12 | if ($schema !== '') { |
|||
375 | 1 | $sql .= ' FROM ' . $this->db->getQuoter()->quoteSimpleTableName($schema); |
|||
376 | } |
||||
377 | |||||
378 | 12 | return $this->db->createCommand($sql)->queryColumn(); |
|||
379 | } |
||||
380 | |||||
381 | /** |
||||
382 | * @throws Exception |
||||
383 | * @throws InvalidConfigException |
||||
384 | * @throws Throwable |
||||
385 | */ |
||||
386 | 1 | protected function findViewNames(string $schema = ''): array |
|||
387 | { |
||||
388 | 1 | $sql = match ($schema) { |
|||
389 | 1 | '' => <<<SQL |
|||
390 | SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema != 'sys' order by table_name |
||||
391 | 1 | SQL, |
|||
392 | 1 | default => <<<SQL |
|||
393 | 1 | SELECT table_name as view FROM information_schema.tables WHERE table_type LIKE 'VIEW' AND table_schema = '$schema' order by table_name |
|||
394 | 1 | SQL, |
|||
395 | 1 | }; |
|||
396 | |||||
397 | /** @psalm-var string[][] $views */ |
||||
398 | 1 | $views = $this->db->createCommand($sql)->queryAll(); |
|||
399 | |||||
400 | 1 | foreach ($views as $key => $view) { |
|||
401 | 1 | $views[$key] = $view['view']; |
|||
402 | } |
||||
403 | |||||
404 | 1 | return $views; |
|||
405 | } |
||||
406 | |||||
407 | /** |
||||
408 | * Returns the cache key for the specified table name. |
||||
409 | * |
||||
410 | * @param string $name The table name. |
||||
411 | * |
||||
412 | * @return array The cache key. |
||||
413 | * |
||||
414 | * @psalm-suppress DeprecatedMethod |
||||
415 | */ |
||||
416 | 267 | protected function getCacheKey(string $name): array |
|||
417 | { |
||||
418 | 267 | return array_merge([self::class], $this->generateCacheKey(), [$this->getRawTableName($name)]); |
|||
0 ignored issues
–
show
|
|||||
419 | } |
||||
420 | |||||
421 | /** |
||||
422 | * Returns the cache tag name. |
||||
423 | * |
||||
424 | * This allows {@see refresh()} to invalidate all cached table schemas. |
||||
425 | * |
||||
426 | * @return string The cache tag name. |
||||
427 | */ |
||||
428 | 227 | protected function getCacheTag(): string |
|||
429 | { |
||||
430 | 227 | return md5(serialize(array_merge([self::class], $this->generateCacheKey()))); |
|||
431 | } |
||||
432 | |||||
433 | /** |
||||
434 | * Gets the `CREATE TABLE` SQL string. |
||||
435 | * |
||||
436 | * @param TableSchemaInterface $table The table metadata. |
||||
437 | * |
||||
438 | * @throws Exception |
||||
439 | * @throws InvalidConfigException |
||||
440 | * @throws Throwable |
||||
441 | * |
||||
442 | * @return string $sql The result of `SHOW CREATE TABLE`. |
||||
443 | */ |
||||
444 | 178 | protected function getCreateTableSql(TableSchemaInterface $table): string |
|||
445 | { |
||||
446 | 178 | $tableName = $table->getFullName() ?? ''; |
|||
447 | |||||
448 | try { |
||||
449 | /** @psalm-var array<array-key, string> $row */ |
||||
450 | 178 | $row = $this->db->createCommand( |
|||
451 | 178 | 'SHOW CREATE TABLE ' . $this->db->getQuoter()->quoteTableName($tableName) |
|||
452 | 178 | )->queryOne(); |
|||
453 | |||||
454 | 160 | if (isset($row['Create Table'])) { |
|||
455 | 158 | $sql = $row['Create Table']; |
|||
456 | } else { |
||||
457 | 4 | $row = array_values($row); |
|||
0 ignored issues
–
show
$row of type null is incompatible with the type array expected by parameter $array of array_values() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
458 | 160 | $sql = $row[1]; |
|||
459 | } |
||||
460 | 37 | } catch (Exception) { |
|||
461 | 37 | $sql = ''; |
|||
462 | } |
||||
463 | |||||
464 | 178 | return $sql; |
|||
465 | } |
||||
466 | |||||
467 | /** |
||||
468 | * Loads the column information into a {@see ColumnSchemaInterface} object. |
||||
469 | * |
||||
470 | * @param array $info The column information. |
||||
471 | * |
||||
472 | * @throws JsonException |
||||
473 | * |
||||
474 | * @return ColumnSchemaInterface The column schema object. |
||||
475 | * |
||||
476 | * @psalm-param ColumnInfoArray $info The column information. |
||||
477 | */ |
||||
478 | 162 | protected function loadColumnSchema(array $info): ColumnSchemaInterface |
|||
479 | { |
||||
480 | 162 | $dbType = $info['type']; |
|||
481 | |||||
482 | 162 | $column = $this->createColumnSchema($info['field']); |
|||
483 | |||||
484 | /** @psalm-var ColumnInfoArray $info */ |
||||
485 | 162 | $column->allowNull($info['null'] === 'YES'); |
|||
486 | 162 | $column->primaryKey(str_contains($info['key'], 'PRI')); |
|||
487 | 162 | $column->autoIncrement(stripos($info['extra'], 'auto_increment') !== false); |
|||
488 | 162 | $column->comment($info['comment']); |
|||
489 | 162 | $column->dbType($dbType); |
|||
490 | 162 | $column->unsigned(stripos($dbType, 'unsigned') !== false); |
|||
491 | 162 | $column->type(self::TYPE_STRING); |
|||
492 | |||||
493 | 162 | if (preg_match('/^(\w+)(?:\(([^)]+)\))?/', $dbType, $matches)) { |
|||
494 | 162 | $type = strtolower($matches[1]); |
|||
495 | |||||
496 | 162 | if (isset(self::TYPE_MAP[$type])) { |
|||
497 | 157 | $column->type(self::TYPE_MAP[$type]); |
|||
498 | } |
||||
499 | |||||
500 | 162 | if (!empty($matches[2])) { |
|||
501 | 126 | if ($type === 'enum') { |
|||
502 | 39 | preg_match_all("/'[^']*'/", $matches[2], $values); |
|||
503 | |||||
504 | 39 | foreach ($values[0] as $i => $value) { |
|||
505 | 39 | $values[$i] = trim($value, "'"); |
|||
506 | } |
||||
507 | |||||
508 | 39 | $column->enumValues($values); |
|||
509 | } else { |
||||
510 | 126 | $values = explode(',', $matches[2]); |
|||
511 | 126 | $column->precision((int) $values[0]); |
|||
512 | 126 | $column->size((int) $values[0]); |
|||
513 | |||||
514 | 126 | if (isset($values[1])) { |
|||
515 | 54 | $column->scale((int) $values[1]); |
|||
516 | } |
||||
517 | |||||
518 | 126 | if ($type === 'bit') { |
|||
519 | 42 | if ($column->getSize() === 1) { |
|||
520 | 42 | $column->type(self::TYPE_BOOLEAN); |
|||
521 | 40 | } elseif ($column->getSize() > 32) { |
|||
522 | 4 | $column->type(self::TYPE_BIGINT); |
|||
523 | 40 | } elseif ($column->getSize() === 32) { |
|||
524 | 4 | $column->type(self::TYPE_INTEGER); |
|||
525 | } |
||||
526 | } |
||||
527 | } |
||||
528 | } |
||||
529 | } |
||||
530 | |||||
531 | // Chapter 2: crutches for MariaDB {@see https://github.com/yiisoft/yii2/issues/19747} |
||||
532 | 162 | $extra = $info['extra']; |
|||
533 | if ( |
||||
534 | 162 | empty($extra) |
|||
535 | 162 | && !empty($info['extra_default_value']) |
|||
536 | 162 | && !str_starts_with($info['extra_default_value'], '\'') |
|||
537 | 162 | && in_array($column->getType(), [ |
|||
538 | 162 | self::TYPE_CHAR, self::TYPE_STRING, self::TYPE_TEXT, |
|||
539 | 162 | self::TYPE_DATETIME, self::TYPE_TIMESTAMP, self::TYPE_TIME, self::TYPE_DATE, |
|||
540 | 162 | ], true) |
|||
541 | ) { |
||||
542 | 1 | $extra = 'DEFAULT_GENERATED'; |
|||
543 | } |
||||
544 | |||||
545 | 162 | $column->extra($extra); |
|||
546 | 162 | $column->phpType($this->getColumnPhpType($column)); |
|||
547 | 162 | $column->defaultValue($this->normalizeDefaultValue($info['default'], $column)); |
|||
548 | |||||
549 | 162 | if (str_starts_with($extra, 'DEFAULT_GENERATED')) { |
|||
550 | 45 | $column->extra(trim(strtoupper(substr($extra, 18)))); |
|||
551 | } |
||||
552 | |||||
553 | 162 | return $column; |
|||
554 | } |
||||
555 | |||||
556 | /** |
||||
557 | * Converts column's default value according to {@see ColumnSchema::phpType} after retrieval from the database. |
||||
558 | * |
||||
559 | * @param string|null $defaultValue The default value retrieved from the database. |
||||
560 | * @param ColumnSchemaInterface $column The column schema object. |
||||
561 | * |
||||
562 | * @return mixed The normalized default value. |
||||
563 | */ |
||||
564 | 162 | private function normalizeDefaultValue(?string $defaultValue, ColumnSchemaInterface $column): mixed |
|||
565 | { |
||||
566 | 162 | if ($defaultValue === null) { |
|||
567 | 149 | return null; |
|||
568 | } |
||||
569 | |||||
570 | 103 | if ($column->isPrimaryKey()) { |
|||
571 | 4 | return $column->phpTypecast($defaultValue); |
|||
572 | } |
||||
573 | |||||
574 | if ( |
||||
575 | 102 | in_array($column->getType(), [self::TYPE_TIMESTAMP, self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME], true) |
|||
576 | 102 | && preg_match('/^current_timestamp(?:\((\d*)\))?$/i', $defaultValue, $matches) === 1 |
|||
577 | ) { |
||||
578 | 42 | return new Expression('CURRENT_TIMESTAMP' . (!empty($matches[1]) ? '(' . $matches[1] . ')' : '')); |
|||
579 | } |
||||
580 | |||||
581 | 100 | if (!empty($defaultValue) && !empty($column->getExtra())) { |
|||
582 | 3 | return new Expression($defaultValue); |
|||
583 | } |
||||
584 | |||||
585 | 98 | if (str_starts_with(strtolower((string) $column->getDbType()), 'bit')) { |
|||
586 | 41 | return $column->phpTypecast(bindec(trim($defaultValue, "b'"))); |
|||
587 | } |
||||
588 | |||||
589 | 96 | return $column->phpTypecast($defaultValue); |
|||
590 | } |
||||
591 | |||||
592 | /** |
||||
593 | * Loads all check constraints for the given table. |
||||
594 | * |
||||
595 | * @param string $tableName The table name. |
||||
596 | * |
||||
597 | * @throws NotSupportedException |
||||
598 | * |
||||
599 | * @return array Check constraints for the given table. |
||||
600 | */ |
||||
601 | 16 | protected function loadTableChecks(string $tableName): array |
|||
602 | { |
||||
603 | 16 | throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.'); |
|||
604 | } |
||||
605 | |||||
606 | /** |
||||
607 | * Loads multiple types of constraints and returns the specified ones. |
||||
608 | * |
||||
609 | * @param string $tableName table name. |
||||
610 | * @param string $returnType return type: |
||||
611 | * - primaryKey |
||||
612 | * - foreignKeys |
||||
613 | * - uniques |
||||
614 | * |
||||
615 | * @throws Exception |
||||
616 | * @throws InvalidConfigException |
||||
617 | * @throws Throwable |
||||
618 | * |
||||
619 | * @psalm-return Constraint[]|ForeignKeyConstraint[]|Constraint|null |
||||
620 | */ |
||||
621 | 72 | private function loadTableConstraints(string $tableName, string $returnType): array|Constraint|null |
|||
622 | { |
||||
623 | 72 | $sql = <<<SQL |
|||
624 | SELECT |
||||
625 | `kcu`.`CONSTRAINT_NAME` AS `name`, |
||||
626 | `kcu`.`COLUMN_NAME` AS `column_name`, |
||||
627 | `tc`.`CONSTRAINT_TYPE` AS `type`, |
||||
628 | CASE |
||||
629 | WHEN :schemaName IS NULL AND `kcu`.`REFERENCED_TABLE_SCHEMA` = DATABASE() THEN NULL |
||||
630 | ELSE `kcu`.`REFERENCED_TABLE_SCHEMA` |
||||
631 | END AS `foreign_table_schema`, |
||||
632 | `kcu`.`REFERENCED_TABLE_NAME` AS `foreign_table_name`, |
||||
633 | `kcu`.`REFERENCED_COLUMN_NAME` AS `foreign_column_name`, |
||||
634 | `rc`.`UPDATE_RULE` AS `on_update`, |
||||
635 | `rc`.`DELETE_RULE` AS `on_delete`, |
||||
636 | `kcu`.`ORDINAL_POSITION` AS `position` |
||||
637 | FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` |
||||
638 | JOIN `information_schema`.`REFERENTIAL_CONSTRAINTS` AS `rc` ON |
||||
639 | `rc`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND |
||||
640 | `rc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND |
||||
641 | `rc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` |
||||
642 | JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON |
||||
643 | `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND |
||||
644 | `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND |
||||
645 | `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND |
||||
646 | `tc`.`CONSTRAINT_TYPE` = 'FOREIGN KEY' |
||||
647 | WHERE |
||||
648 | `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND |
||||
649 | `kcu`.`CONSTRAINT_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND |
||||
650 | `kcu`.`TABLE_NAME` = :tableName |
||||
651 | UNION |
||||
652 | SELECT |
||||
653 | `kcu`.`CONSTRAINT_NAME` AS `name`, |
||||
654 | `kcu`.`COLUMN_NAME` AS `column_name`, |
||||
655 | `tc`.`CONSTRAINT_TYPE` AS `type`, |
||||
656 | NULL AS `foreign_table_schema`, |
||||
657 | NULL AS `foreign_table_name`, |
||||
658 | NULL AS `foreign_column_name`, |
||||
659 | NULL AS `on_update`, |
||||
660 | NULL AS `on_delete`, |
||||
661 | `kcu`.`ORDINAL_POSITION` AS `position` |
||||
662 | FROM `information_schema`.`KEY_COLUMN_USAGE` AS `kcu` |
||||
663 | JOIN `information_schema`.`TABLE_CONSTRAINTS` AS `tc` ON |
||||
664 | `tc`.`TABLE_SCHEMA` = `kcu`.`TABLE_SCHEMA` AND |
||||
665 | `tc`.`TABLE_NAME` = `kcu`.`TABLE_NAME` AND |
||||
666 | `tc`.`CONSTRAINT_NAME` = `kcu`.`CONSTRAINT_NAME` AND |
||||
667 | `tc`.`CONSTRAINT_TYPE` IN ('PRIMARY KEY', 'UNIQUE') |
||||
668 | WHERE |
||||
669 | `kcu`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND |
||||
670 | `kcu`.`TABLE_NAME` = :tableName |
||||
671 | ORDER BY `position` ASC |
||||
672 | 72 | SQL; |
|||
673 | |||||
674 | 72 | $resolvedName = $this->resolveTableName($tableName); |
|||
675 | 72 | $constraints = $this->db->createCommand($sql, [ |
|||
676 | 72 | ':schemaName' => $resolvedName->getSchemaName(), |
|||
677 | 72 | ':tableName' => $resolvedName->getName(), |
|||
678 | 72 | ])->queryAll(); |
|||
679 | |||||
680 | /** @psalm-var array[][] $constraints */ |
||||
681 | 72 | $constraints = array_map('array_change_key_case', $constraints); |
|||
682 | 72 | $constraints = DbArrayHelper::index($constraints, null, ['type', 'name']); |
|||
683 | |||||
684 | 72 | $result = [ |
|||
685 | 72 | self::PRIMARY_KEY => null, |
|||
686 | 72 | self::FOREIGN_KEYS => [], |
|||
687 | 72 | self::UNIQUES => [], |
|||
688 | 72 | ]; |
|||
689 | |||||
690 | /** |
||||
691 | * @psalm-var string $type |
||||
692 | * @psalm-var array $names |
||||
693 | */ |
||||
694 | 72 | foreach ($constraints as $type => $names) { |
|||
695 | /** |
||||
696 | * @psalm-var object|string|null $name |
||||
697 | * @psalm-var ConstraintArray $constraint |
||||
698 | */ |
||||
699 | 67 | foreach ($names as $name => $constraint) { |
|||
700 | switch ($type) { |
||||
701 | 67 | case 'PRIMARY KEY': |
|||
702 | 48 | $result[self::PRIMARY_KEY] = (new Constraint()) |
|||
703 | 48 | ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name')); |
|||
704 | 48 | break; |
|||
705 | 59 | case 'FOREIGN KEY': |
|||
706 | 16 | $result[self::FOREIGN_KEYS][] = (new ForeignKeyConstraint()) |
|||
707 | 16 | ->foreignSchemaName($constraint[0]['foreign_table_schema']) |
|||
708 | 16 | ->foreignTableName($constraint[0]['foreign_table_name']) |
|||
709 | 16 | ->foreignColumnNames(DbArrayHelper::getColumn($constraint, 'foreign_column_name')) |
|||
710 | 16 | ->onDelete($constraint[0]['on_delete']) |
|||
711 | 16 | ->onUpdate($constraint[0]['on_update']) |
|||
712 | 16 | ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name')) |
|||
713 | 16 | ->name($name); |
|||
714 | 16 | break; |
|||
715 | 49 | case 'UNIQUE': |
|||
716 | 49 | $result[self::UNIQUES][] = (new Constraint()) |
|||
717 | 49 | ->columnNames(DbArrayHelper::getColumn($constraint, 'column_name')) |
|||
718 | 49 | ->name($name); |
|||
719 | 49 | break; |
|||
720 | } |
||||
721 | } |
||||
722 | } |
||||
723 | |||||
724 | 72 | foreach ($result as $type => $data) { |
|||
725 | 72 | $this->setTableMetadata($tableName, $type, $data); |
|||
726 | } |
||||
727 | |||||
728 | 72 | return $result[$returnType]; |
|||
729 | } |
||||
730 | |||||
731 | /** |
||||
732 | * Loads all default value constraints for the given table. |
||||
733 | * |
||||
734 | * @param string $tableName The table name. |
||||
735 | * |
||||
736 | * @throws NotSupportedException |
||||
737 | * |
||||
738 | * @return array Default value constraints for the given table. |
||||
739 | */ |
||||
740 | 15 | protected function loadTableDefaultValues(string $tableName): array |
|||
741 | { |
||||
742 | 15 | throw new NotSupportedException(__METHOD__ . ' is not supported by MySQL.'); |
|||
743 | } |
||||
744 | |||||
745 | /** |
||||
746 | * Loads all foreign keys for the given table. |
||||
747 | * |
||||
748 | * @param string $tableName The table name. |
||||
749 | * |
||||
750 | * @throws Exception |
||||
751 | * @throws InvalidConfigException |
||||
752 | * @throws Throwable |
||||
753 | * |
||||
754 | * @return array Foreign keys for the given table. |
||||
755 | */ |
||||
756 | 9 | protected function loadTableForeignKeys(string $tableName): array |
|||
757 | { |
||||
758 | 9 | $tableForeignKeys = $this->loadTableConstraints($tableName, self::FOREIGN_KEYS); |
|||
759 | 9 | return is_array($tableForeignKeys) ? $tableForeignKeys : []; |
|||
760 | } |
||||
761 | |||||
762 | /** |
||||
763 | * Loads all indexes for the given table. |
||||
764 | * |
||||
765 | * @param string $tableName The table name. |
||||
766 | * |
||||
767 | * @throws Exception |
||||
768 | * @throws InvalidConfigException |
||||
769 | * @throws Throwable |
||||
770 | * |
||||
771 | * @return IndexConstraint[] Indexes for the given table. |
||||
772 | */ |
||||
773 | 39 | protected function loadTableIndexes(string $tableName): array |
|||
774 | { |
||||
775 | 39 | $sql = <<<SQL |
|||
776 | SELECT |
||||
777 | `s`.`INDEX_NAME` AS `name`, |
||||
778 | `s`.`COLUMN_NAME` AS `column_name`, |
||||
779 | `s`.`NON_UNIQUE` ^ 1 AS `index_is_unique`, |
||||
780 | `s`.`INDEX_NAME` = 'PRIMARY' AS `index_is_primary` |
||||
781 | FROM `information_schema`.`STATISTICS` AS `s` |
||||
782 | WHERE |
||||
783 | `s`.`TABLE_SCHEMA` = COALESCE(:schemaName, DATABASE()) AND |
||||
784 | `s`.`INDEX_SCHEMA` = `s`.`TABLE_SCHEMA` AND |
||||
785 | `s`.`TABLE_NAME` = :tableName |
||||
786 | ORDER BY `s`.`SEQ_IN_INDEX` ASC |
||||
787 | 39 | SQL; |
|||
788 | |||||
789 | 39 | $resolvedName = $this->resolveTableName($tableName); |
|||
790 | 39 | $indexes = $this->db->createCommand($sql, [ |
|||
791 | 39 | ':schemaName' => $resolvedName->getSchemaName(), |
|||
792 | 39 | ':tableName' => $resolvedName->getName(), |
|||
793 | 39 | ])->queryAll(); |
|||
794 | |||||
795 | /** @psalm-var array[] $indexes */ |
||||
796 | 39 | $indexes = array_map('array_change_key_case', $indexes); |
|||
797 | 39 | $indexes = DbArrayHelper::index($indexes, null, ['name']); |
|||
798 | 39 | $result = []; |
|||
799 | |||||
800 | /** |
||||
801 | * @psalm-var object|string|null $name |
||||
802 | * @psalm-var array[] $index |
||||
803 | */ |
||||
804 | 39 | foreach ($indexes as $name => $index) { |
|||
805 | 39 | $ic = new IndexConstraint(); |
|||
806 | |||||
807 | 39 | $ic->primary((bool) $index[0]['index_is_primary']); |
|||
808 | 39 | $ic->unique((bool) $index[0]['index_is_unique']); |
|||
809 | 39 | $ic->name($name !== 'PRIMARY' ? $name : null); |
|||
810 | 39 | $ic->columnNames(DbArrayHelper::getColumn($index, 'column_name')); |
|||
811 | |||||
812 | 39 | $result[] = $ic; |
|||
813 | } |
||||
814 | |||||
815 | 39 | return $result; |
|||
816 | } |
||||
817 | |||||
818 | /** |
||||
819 | * Loads a primary key for the given table. |
||||
820 | * |
||||
821 | * @param string $tableName The table name. |
||||
822 | * |
||||
823 | * @throws Exception |
||||
824 | * @throws InvalidConfigException |
||||
825 | * @throws Throwable |
||||
826 | * |
||||
827 | * @return Constraint|null Primary key for the given table, `null` if the table has no primary key.* |
||||
828 | */ |
||||
829 | 46 | protected function loadTablePrimaryKey(string $tableName): Constraint|null |
|||
830 | { |
||||
831 | 46 | $tablePrimaryKey = $this->loadTableConstraints($tableName, self::PRIMARY_KEY); |
|||
832 | 46 | return $tablePrimaryKey instanceof Constraint ? $tablePrimaryKey : null; |
|||
833 | } |
||||
834 | |||||
835 | /** |
||||
836 | * Loads the metadata for the specified table. |
||||
837 | * |
||||
838 | * @param string $name The table name. |
||||
839 | * |
||||
840 | * @throws Exception |
||||
841 | * @throws Throwable |
||||
842 | * |
||||
843 | * @return TableSchemaInterface|null DBMS-dependent table metadata, `null` if the table doesn't exist. |
||||
844 | */ |
||||
845 | 178 | protected function loadTableSchema(string $name): TableSchemaInterface|null |
|||
846 | { |
||||
847 | 178 | $table = $this->resolveTableName($name); |
|||
848 | 178 | $this->resolveTableCreateSql($table); |
|||
849 | 178 | $this->findTableComment($table); |
|||
850 | |||||
851 | 178 | if ($this->findColumns($table)) { |
|||
852 | 160 | $this->findConstraints($table); |
|||
853 | |||||
854 | 160 | return $table; |
|||
855 | } |
||||
856 | |||||
857 | 37 | return null; |
|||
858 | } |
||||
859 | |||||
860 | /** |
||||
861 | * Loads all unique constraints for the given table. |
||||
862 | * |
||||
863 | * @param string $tableName The table name. |
||||
864 | * |
||||
865 | * @throws Exception |
||||
866 | * @throws InvalidConfigException |
||||
867 | * @throws Throwable |
||||
868 | * |
||||
869 | * @return array Unique constraints for the given table. |
||||
870 | */ |
||||
871 | 17 | protected function loadTableUniques(string $tableName): array |
|||
872 | { |
||||
873 | 17 | $tableUniques = $this->loadTableConstraints($tableName, self::UNIQUES); |
|||
874 | 17 | return is_array($tableUniques) ? $tableUniques : []; |
|||
875 | } |
||||
876 | |||||
877 | /** |
||||
878 | * Resolves the table name and schema name (if any). |
||||
879 | * |
||||
880 | * @param string $name The table name. |
||||
881 | * |
||||
882 | * @see TableSchemaInterface |
||||
883 | */ |
||||
884 | 225 | protected function resolveTableName(string $name): TableSchemaInterface |
|||
885 | { |
||||
886 | 225 | $resolvedName = new TableSchema(); |
|||
887 | |||||
888 | 225 | $parts = array_reverse($this->db->getQuoter()->getTableNameParts($name)); |
|||
889 | 225 | $resolvedName->name($parts[0] ?? ''); |
|||
890 | 225 | $resolvedName->schemaName($parts[1] ?? $this->defaultSchema); |
|||
891 | 225 | $resolvedName->fullName( |
|||
892 | 225 | $resolvedName->getSchemaName() !== $this->defaultSchema ? |
|||
893 | 225 | implode('.', array_reverse($parts)) : $resolvedName->getName() |
|||
894 | 225 | ); |
|||
895 | |||||
896 | 225 | return $resolvedName; |
|||
897 | } |
||||
898 | |||||
899 | /** |
||||
900 | * @throws Exception |
||||
901 | * @throws InvalidConfigException |
||||
902 | * @throws Throwable |
||||
903 | */ |
||||
904 | 178 | protected function resolveTableCreateSql(TableSchemaInterface $table): void |
|||
905 | { |
||||
906 | 178 | $sql = $this->getCreateTableSql($table); |
|||
907 | 178 | $table->createSql($sql); |
|||
908 | } |
||||
909 | |||||
910 | /** |
||||
911 | * Creates a column schema for the database. |
||||
912 | * |
||||
913 | * This method may be overridden by child classes to create a DBMS-specific column schema. |
||||
914 | * |
||||
915 | * @param string $name Name of the column. |
||||
916 | */ |
||||
917 | 162 | private function createColumnSchema(string $name): ColumnSchema |
|||
918 | { |
||||
919 | 162 | return new ColumnSchema($name); |
|||
920 | } |
||||
921 | |||||
922 | /** |
||||
923 | * @throws Exception |
||||
924 | * @throws InvalidConfigException |
||||
925 | * @throws Throwable |
||||
926 | */ |
||||
927 | 160 | private function getJsonColumns(TableSchemaInterface $table): array |
|||
928 | { |
||||
929 | 160 | $sql = $this->getCreateTableSql($table); |
|||
930 | 160 | $result = []; |
|||
931 | 160 | $regexp = '/json_valid\([\`"](.+)[\`"]\s*\)/mi'; |
|||
932 | |||||
933 | 160 | if (preg_match_all($regexp, $sql, $matches, PREG_SET_ORDER) > 0) { |
|||
934 | foreach ($matches as $match) { |
||||
935 | $result[] = $match[1]; |
||||
936 | } |
||||
937 | } |
||||
938 | |||||
939 | 160 | return $result; |
|||
940 | } |
||||
941 | } |
||||
942 |
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.