|
1 | 1 | import type { Attribute, BuiltinType } from '@zenstackhq/language/ast'; |
2 | 2 | import { DataFieldAttributeFactory } from '@zenstackhq/language/factory'; |
3 | | -import { getAttributeRef, getDbName, getFunctionRef } from '../utils'; |
| 3 | +import { getAttributeRef, getDbName, getFunctionRef, normalizeDecimalDefault, normalizeFloatDefault } from '../utils'; |
4 | 4 | import type { IntrospectedEnum, IntrospectedSchema, IntrospectedTable, IntrospectionProvider } from './provider'; |
5 | 5 | import { CliError } from '../../../cli-error'; |
6 | 6 |
|
@@ -139,19 +139,11 @@ export const mysql: IntrospectionProvider = { |
139 | 139 | const indexes = typeof row.indexes === 'string' ? JSON.parse(row.indexes) : row.indexes; |
140 | 140 |
|
141 | 141 | // Sort columns by ordinal_position to preserve database column order |
142 | | - const sortedColumns = (columns || []) |
143 | | - .sort( |
144 | | - (a: { ordinal_position?: number }, b: { ordinal_position?: number }) => |
145 | | - (a.ordinal_position ?? 0) - (b.ordinal_position ?? 0) |
146 | | - ) |
147 | | - .map((col: { options?: string | string[] | null }) => ({ |
148 | | - ...col, |
149 | | - // Parse enum options from COLUMN_TYPE if present (e.g., "enum('val1','val2')") |
150 | | - options: |
151 | | - typeof col.options === 'string' |
152 | | - ? parseEnumValues(col.options) |
153 | | - : col.options ?? [], |
154 | | - })); |
| 142 | + const sortedColumns = (columns || []) |
| 143 | + .sort( |
| 144 | + (a: { ordinal_position?: number }, b: { ordinal_position?: number }) => |
| 145 | + (a.ordinal_position ?? 0) - (b.ordinal_position ?? 0) |
| 146 | + ); |
155 | 147 |
|
156 | 148 | // Filter out auto-generated FK indexes (MySQL creates these automatically) |
157 | 149 | // Pattern: {Table}_{column}_fkey for single-column FK indexes |
@@ -291,126 +283,171 @@ export const mysql: IntrospectionProvider = { |
291 | 283 |
|
292 | 284 | function getTableIntrospectionQuery(databaseName: string) { |
293 | 285 | // Note: We use subqueries with ORDER BY before JSON_ARRAYAGG to ensure ordering |
294 | | - // since MySQL < 8.0.21 doesn't support ORDER BY inside JSON_ARRAYAGG |
295 | | - // MySQL doesn't support multi-schema, so we don't include schema in the result |
| 286 | + // since MySQL < 8.0.21 doesn't support ORDER BY inside JSON_ARRAYAGG. |
| 287 | + // MySQL doesn't support multi-schema, so we don't include schema in the result. |
296 | 288 | return ` |
| 289 | +-- Main query: one row per table/view with columns and indexes as nested JSON arrays. |
| 290 | +-- Uses INFORMATION_SCHEMA which is MySQL's standard metadata catalog. |
297 | 291 | SELECT |
298 | | - t.TABLE_NAME AS \`name\`, |
299 | | - CASE t.TABLE_TYPE |
| 292 | + t.TABLE_NAME AS \`name\`, -- table or view name |
| 293 | + CASE t.TABLE_TYPE -- map MySQL table type strings to our internal types |
300 | 294 | WHEN 'BASE TABLE' THEN 'table' |
301 | 295 | WHEN 'VIEW' THEN 'view' |
302 | 296 | ELSE NULL |
303 | 297 | END AS \`type\`, |
304 | | - CASE |
| 298 | + CASE -- for views, retrieve the SQL definition |
305 | 299 | WHEN t.TABLE_TYPE = 'VIEW' THEN v.VIEW_DEFINITION |
306 | 300 | ELSE NULL |
307 | 301 | END AS \`definition\`, |
| 302 | +
|
| 303 | + -- ===== COLUMNS subquery ===== |
| 304 | + -- Wraps an ordered subquery in JSON_ARRAYAGG to produce a JSON array of column objects. |
308 | 305 | ( |
309 | 306 | SELECT JSON_ARRAYAGG(col_json) |
310 | 307 | FROM ( |
311 | 308 | SELECT JSON_OBJECT( |
312 | | - 'ordinal_position', c.ORDINAL_POSITION, |
313 | | - 'name', c.COLUMN_NAME, |
| 309 | + 'ordinal_position', c.ORDINAL_POSITION, -- column position (used for sorting) |
| 310 | + 'name', c.COLUMN_NAME, -- column name |
| 311 | +
|
| 312 | + -- datatype: special-case tinyint(1) as 'boolean' (MySQL's boolean convention), |
| 313 | + -- otherwise use the DATA_TYPE (e.g., 'int', 'varchar', 'datetime') |
314 | 314 | 'datatype', CASE |
315 | 315 | WHEN c.DATA_TYPE = 'tinyint' AND c.COLUMN_TYPE = 'tinyint(1)' THEN 'boolean' |
316 | 316 | ELSE c.DATA_TYPE |
317 | 317 | END, |
| 318 | +
|
| 319 | + -- datatype_name: for enum columns, generate a synthetic name "TableName_ColumnName" |
| 320 | + -- (MySQL doesn't have named enum types like PostgreSQL) |
318 | 321 | 'datatype_name', CASE |
319 | 322 | WHEN c.DATA_TYPE = 'enum' THEN CONCAT(t.TABLE_NAME, '_', c.COLUMN_NAME) |
320 | 323 | ELSE NULL |
321 | 324 | END, |
322 | | - 'datatype_schema', '', |
323 | | - 'length', c.CHARACTER_MAXIMUM_LENGTH, |
324 | | - 'precision', COALESCE(c.NUMERIC_PRECISION, c.DATETIME_PRECISION), |
325 | | - 'nullable', c.IS_NULLABLE = 'YES', |
| 325 | +
|
| 326 | + 'datatype_schema', '', -- MySQL doesn't support multi-schema |
| 327 | + 'length', c.CHARACTER_MAXIMUM_LENGTH, -- max length for string types (e.g., VARCHAR(255) -> 255) |
| 328 | + 'precision', COALESCE(c.NUMERIC_PRECISION, c.DATETIME_PRECISION), -- numeric or datetime precision |
| 329 | +
|
| 330 | + 'nullable', c.IS_NULLABLE = 'YES', -- true if column allows NULL |
| 331 | +
|
| 332 | + -- default: for auto_increment columns, report 'auto_increment' instead of NULL; |
| 333 | + -- otherwise use the COLUMN_DEFAULT value |
326 | 334 | 'default', CASE |
327 | 335 | WHEN c.EXTRA LIKE '%auto_increment%' THEN 'auto_increment' |
328 | 336 | ELSE c.COLUMN_DEFAULT |
329 | 337 | END, |
330 | | - 'pk', c.COLUMN_KEY = 'PRI', |
331 | | - 'unique', c.COLUMN_KEY = 'UNI', |
| 338 | +
|
| 339 | + 'pk', c.COLUMN_KEY = 'PRI', -- true if column is part of the primary key |
| 340 | + 'unique', c.COLUMN_KEY = 'UNI', -- true if column has a unique constraint |
332 | 341 | 'unique_name', CASE WHEN c.COLUMN_KEY = 'UNI' THEN c.COLUMN_NAME ELSE NULL END, |
| 342 | +
|
| 343 | + -- computed: true if column has a generation expression (virtual or stored) |
333 | 344 | 'computed', c.GENERATION_EXPRESSION IS NOT NULL AND c.GENERATION_EXPRESSION != '', |
| 345 | +
|
| 346 | + -- options: for enum columns, the full COLUMN_TYPE string (e.g., "enum('a','b','c')") |
| 347 | + -- which gets parsed into individual values later |
334 | 348 | 'options', CASE |
335 | 349 | WHEN c.DATA_TYPE = 'enum' THEN c.COLUMN_TYPE |
336 | 350 | ELSE NULL |
337 | 351 | END, |
338 | | - 'foreign_key_schema', NULL, |
339 | | - 'foreign_key_table', kcu_fk.REFERENCED_TABLE_NAME, |
340 | | - 'foreign_key_column', kcu_fk.REFERENCED_COLUMN_NAME, |
341 | | - 'foreign_key_name', kcu_fk.CONSTRAINT_NAME, |
342 | | - 'foreign_key_on_update', rc.UPDATE_RULE, |
343 | | - 'foreign_key_on_delete', rc.DELETE_RULE |
| 352 | +
|
| 353 | + -- Foreign key info (NULL if column is not part of a FK) |
| 354 | + 'foreign_key_schema', NULL, -- MySQL doesn't support cross-schema FKs here |
| 355 | + 'foreign_key_table', kcu_fk.REFERENCED_TABLE_NAME, -- referenced table |
| 356 | + 'foreign_key_column', kcu_fk.REFERENCED_COLUMN_NAME, -- referenced column |
| 357 | + 'foreign_key_name', kcu_fk.CONSTRAINT_NAME, -- FK constraint name |
| 358 | + 'foreign_key_on_update', rc.UPDATE_RULE, -- referential action on update (CASCADE, SET NULL, etc.) |
| 359 | + 'foreign_key_on_delete', rc.DELETE_RULE -- referential action on delete |
344 | 360 | ) AS col_json |
345 | | - FROM INFORMATION_SCHEMA.COLUMNS c |
| 361 | +
|
| 362 | + FROM INFORMATION_SCHEMA.COLUMNS c -- one row per column in the database |
| 363 | +
|
| 364 | + -- Join KEY_COLUMN_USAGE to find foreign key references for this column. |
| 365 | + -- Filter to only FK entries (REFERENCED_TABLE_NAME IS NOT NULL). |
346 | 366 | LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu_fk |
347 | 367 | ON c.TABLE_SCHEMA = kcu_fk.TABLE_SCHEMA |
348 | 368 | AND c.TABLE_NAME = kcu_fk.TABLE_NAME |
349 | 369 | AND c.COLUMN_NAME = kcu_fk.COLUMN_NAME |
350 | 370 | AND kcu_fk.REFERENCED_TABLE_NAME IS NOT NULL |
| 371 | +
|
| 372 | + -- Join REFERENTIAL_CONSTRAINTS to get ON UPDATE / ON DELETE rules for the FK. |
351 | 373 | LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc |
352 | 374 | ON kcu_fk.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA |
353 | 375 | AND kcu_fk.CONSTRAINT_NAME = rc.CONSTRAINT_NAME |
| 376 | +
|
354 | 377 | WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA |
355 | 378 | AND c.TABLE_NAME = t.TABLE_NAME |
356 | | - ORDER BY c.ORDINAL_POSITION |
| 379 | + ORDER BY c.ORDINAL_POSITION -- preserve original column order |
357 | 380 | ) AS cols_ordered |
358 | 381 | ) AS \`columns\`, |
| 382 | +
|
| 383 | + -- ===== INDEXES subquery ===== |
| 384 | + -- Aggregates all indexes for this table into a JSON array. |
359 | 385 | ( |
360 | 386 | SELECT JSON_ARRAYAGG(idx_json) |
361 | 387 | FROM ( |
362 | 388 | SELECT JSON_OBJECT( |
363 | | - 'name', s.INDEX_NAME, |
364 | | - 'method', s.INDEX_TYPE, |
365 | | - 'unique', s.NON_UNIQUE = 0, |
366 | | - 'primary', s.INDEX_NAME = 'PRIMARY', |
367 | | - 'valid', TRUE, |
368 | | - 'ready', TRUE, |
369 | | - 'partial', FALSE, |
370 | | - 'predicate', NULL, |
| 389 | + 'name', s.INDEX_NAME, -- index name (e.g., 'PRIMARY', 'idx_email') |
| 390 | + 'method', s.INDEX_TYPE, -- index type (e.g., 'BTREE', 'HASH', 'FULLTEXT') |
| 391 | + 'unique', s.NON_UNIQUE = 0, -- NON_UNIQUE=0 means it IS unique |
| 392 | + 'primary', s.INDEX_NAME = 'PRIMARY', -- MySQL names the PK index 'PRIMARY' |
| 393 | + 'valid', TRUE, -- MySQL doesn't expose index validity status |
| 394 | + 'ready', TRUE, -- MySQL doesn't expose index readiness status |
| 395 | + 'partial', FALSE, -- MySQL doesn't support partial indexes |
| 396 | + 'predicate', NULL, -- no WHERE clause on indexes in MySQL |
| 397 | +
|
| 398 | + -- Index columns: nested subquery for columns in this index |
371 | 399 | 'columns', ( |
372 | 400 | SELECT JSON_ARRAYAGG(idx_col_json) |
373 | 401 | FROM ( |
374 | 402 | SELECT JSON_OBJECT( |
375 | | - 'name', s2.COLUMN_NAME, |
376 | | - 'expression', NULL, |
| 403 | + 'name', s2.COLUMN_NAME, -- column name in the index |
| 404 | + 'expression', NULL, -- MySQL doesn't expose expression indexes via STATISTICS |
| 405 | + -- COLLATION: 'A' = ascending, 'D' = descending, NULL = not sorted |
377 | 406 | 'order', CASE s2.COLLATION WHEN 'A' THEN 'ASC' WHEN 'D' THEN 'DESC' ELSE NULL END, |
378 | | - 'nulls', NULL |
| 407 | + 'nulls', NULL -- MySQL doesn't expose NULLS FIRST/LAST |
379 | 408 | ) AS idx_col_json |
380 | | - FROM INFORMATION_SCHEMA.STATISTICS s2 |
| 409 | + FROM INFORMATION_SCHEMA.STATISTICS s2 -- one row per column per index |
381 | 410 | WHERE s2.TABLE_SCHEMA = s.TABLE_SCHEMA |
382 | 411 | AND s2.TABLE_NAME = s.TABLE_NAME |
383 | 412 | AND s2.INDEX_NAME = s.INDEX_NAME |
384 | | - ORDER BY s2.SEQ_IN_INDEX |
| 413 | + ORDER BY s2.SEQ_IN_INDEX -- preserve column order within the index |
385 | 414 | ) AS idx_cols_ordered |
386 | 415 | ) |
387 | 416 | ) AS idx_json |
388 | 417 | FROM ( |
| 418 | + -- Deduplicate: STATISTICS has one row per (index, column), but we need one row per index. |
| 419 | + -- DISTINCT on INDEX_NAME gives us one entry per index with its metadata. |
389 | 420 | SELECT DISTINCT INDEX_NAME, INDEX_TYPE, NON_UNIQUE, TABLE_SCHEMA, TABLE_NAME |
390 | 421 | FROM INFORMATION_SCHEMA.STATISTICS |
391 | 422 | WHERE TABLE_SCHEMA = t.TABLE_SCHEMA AND TABLE_NAME = t.TABLE_NAME |
392 | 423 | ) s |
393 | 424 | ) AS idxs_ordered |
394 | 425 | ) AS \`indexes\` |
| 426 | +
|
| 427 | +-- === Main FROM: INFORMATION_SCHEMA.TABLES lists all tables and views === |
395 | 428 | FROM INFORMATION_SCHEMA.TABLES t |
| 429 | +-- Join VIEWS to get VIEW_DEFINITION for view tables |
396 | 430 | LEFT JOIN INFORMATION_SCHEMA.VIEWS v |
397 | 431 | ON t.TABLE_SCHEMA = v.TABLE_SCHEMA AND t.TABLE_NAME = v.TABLE_NAME |
398 | | -WHERE t.TABLE_SCHEMA = '${databaseName}' |
399 | | - AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW') |
400 | | - AND t.TABLE_NAME <> '_prisma_migrations' |
| 432 | +WHERE t.TABLE_SCHEMA = '${databaseName}' -- only the target database |
| 433 | + AND t.TABLE_TYPE IN ('BASE TABLE', 'VIEW') -- exclude system tables like SYSTEM VIEW |
| 434 | + AND t.TABLE_NAME <> '_prisma_migrations' -- exclude Prisma migration tracking table |
401 | 435 | ORDER BY t.TABLE_NAME; |
402 | 436 | `; |
403 | 437 | } |
404 | 438 |
|
405 | 439 | function getEnumIntrospectionQuery(databaseName: string) { |
| 440 | + // MySQL doesn't have standalone enum types like PostgreSQL's CREATE TYPE. |
| 441 | + // Instead, enum values are embedded in column definitions (e.g., COLUMN_TYPE = "enum('a','b','c')"). |
| 442 | + // This query finds all enum columns so we can extract their allowed values. |
406 | 443 | return ` |
407 | 444 | SELECT |
408 | | - c.TABLE_NAME AS table_name, |
409 | | - c.COLUMN_NAME AS column_name, |
410 | | - c.COLUMN_TYPE AS column_type |
| 445 | + c.TABLE_NAME AS table_name, -- table containing the enum column |
| 446 | + c.COLUMN_NAME AS column_name, -- column name |
| 447 | + c.COLUMN_TYPE AS column_type -- full type string including values (e.g., "enum('val1','val2')") |
411 | 448 | FROM INFORMATION_SCHEMA.COLUMNS c |
412 | | -WHERE c.TABLE_SCHEMA = '${databaseName}' |
413 | | - AND c.DATA_TYPE = 'enum' |
| 449 | +WHERE c.TABLE_SCHEMA = '${databaseName}' -- only the target database |
| 450 | + AND c.DATA_TYPE = 'enum' -- only enum columns |
414 | 451 | ORDER BY c.TABLE_NAME, c.COLUMN_NAME; |
415 | 452 | `; |
416 | 453 | } |
|
0 commit comments