Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit c135c69

Browse files
committed
docs(cli): add documentation comments to SQL introspection queries
1 parent a5827ae commit c135c69

4 files changed

Lines changed: 244 additions & 133 deletions

File tree

packages/cli/src/actions/pull/provider/mysql.ts

Lines changed: 93 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Attribute, BuiltinType } from '@zenstackhq/language/ast';
22
import { DataFieldAttributeFactory } from '@zenstackhq/language/factory';
3-
import { getAttributeRef, getDbName, getFunctionRef } from '../utils';
3+
import { getAttributeRef, getDbName, getFunctionRef, normalizeDecimalDefault, normalizeFloatDefault } from '../utils';
44
import type { IntrospectedEnum, IntrospectedSchema, IntrospectedTable, IntrospectionProvider } from './provider';
55
import { CliError } from '../../../cli-error';
66

@@ -139,19 +139,11 @@ export const mysql: IntrospectionProvider = {
139139
const indexes = typeof row.indexes === 'string' ? JSON.parse(row.indexes) : row.indexes;
140140

141141
// 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+
);
155147

156148
// Filter out auto-generated FK indexes (MySQL creates these automatically)
157149
// Pattern: {Table}_{column}_fkey for single-column FK indexes
@@ -291,126 +283,171 @@ export const mysql: IntrospectionProvider = {
291283

292284
function getTableIntrospectionQuery(databaseName: string) {
293285
// 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.
296288
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.
297291
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
300294
WHEN 'BASE TABLE' THEN 'table'
301295
WHEN 'VIEW' THEN 'view'
302296
ELSE NULL
303297
END AS \`type\`,
304-
CASE
298+
CASE -- for views, retrieve the SQL definition
305299
WHEN t.TABLE_TYPE = 'VIEW' THEN v.VIEW_DEFINITION
306300
ELSE NULL
307301
END AS \`definition\`,
302+
303+
-- ===== COLUMNS subquery =====
304+
-- Wraps an ordered subquery in JSON_ARRAYAGG to produce a JSON array of column objects.
308305
(
309306
SELECT JSON_ARRAYAGG(col_json)
310307
FROM (
311308
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')
314314
'datatype', CASE
315315
WHEN c.DATA_TYPE = 'tinyint' AND c.COLUMN_TYPE = 'tinyint(1)' THEN 'boolean'
316316
ELSE c.DATA_TYPE
317317
END,
318+
319+
-- datatype_name: for enum columns, generate a synthetic name "TableName_ColumnName"
320+
-- (MySQL doesn't have named enum types like PostgreSQL)
318321
'datatype_name', CASE
319322
WHEN c.DATA_TYPE = 'enum' THEN CONCAT(t.TABLE_NAME, '_', c.COLUMN_NAME)
320323
ELSE NULL
321324
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
326334
'default', CASE
327335
WHEN c.EXTRA LIKE '%auto_increment%' THEN 'auto_increment'
328336
ELSE c.COLUMN_DEFAULT
329337
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
332341
'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)
333344
'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
334348
'options', CASE
335349
WHEN c.DATA_TYPE = 'enum' THEN c.COLUMN_TYPE
336350
ELSE NULL
337351
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
344360
) 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).
346366
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu_fk
347367
ON c.TABLE_SCHEMA = kcu_fk.TABLE_SCHEMA
348368
AND c.TABLE_NAME = kcu_fk.TABLE_NAME
349369
AND c.COLUMN_NAME = kcu_fk.COLUMN_NAME
350370
AND kcu_fk.REFERENCED_TABLE_NAME IS NOT NULL
371+
372+
-- Join REFERENTIAL_CONSTRAINTS to get ON UPDATE / ON DELETE rules for the FK.
351373
LEFT JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
352374
ON kcu_fk.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
353375
AND kcu_fk.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
376+
354377
WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA
355378
AND c.TABLE_NAME = t.TABLE_NAME
356-
ORDER BY c.ORDINAL_POSITION
379+
ORDER BY c.ORDINAL_POSITION -- preserve original column order
357380
) AS cols_ordered
358381
) AS \`columns\`,
382+
383+
-- ===== INDEXES subquery =====
384+
-- Aggregates all indexes for this table into a JSON array.
359385
(
360386
SELECT JSON_ARRAYAGG(idx_json)
361387
FROM (
362388
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
371399
'columns', (
372400
SELECT JSON_ARRAYAGG(idx_col_json)
373401
FROM (
374402
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
377406
'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
379408
) AS idx_col_json
380-
FROM INFORMATION_SCHEMA.STATISTICS s2
409+
FROM INFORMATION_SCHEMA.STATISTICS s2 -- one row per column per index
381410
WHERE s2.TABLE_SCHEMA = s.TABLE_SCHEMA
382411
AND s2.TABLE_NAME = s.TABLE_NAME
383412
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
385414
) AS idx_cols_ordered
386415
)
387416
) AS idx_json
388417
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.
389420
SELECT DISTINCT INDEX_NAME, INDEX_TYPE, NON_UNIQUE, TABLE_SCHEMA, TABLE_NAME
390421
FROM INFORMATION_SCHEMA.STATISTICS
391422
WHERE TABLE_SCHEMA = t.TABLE_SCHEMA AND TABLE_NAME = t.TABLE_NAME
392423
) s
393424
) AS idxs_ordered
394425
) AS \`indexes\`
426+
427+
-- === Main FROM: INFORMATION_SCHEMA.TABLES lists all tables and views ===
395428
FROM INFORMATION_SCHEMA.TABLES t
429+
-- Join VIEWS to get VIEW_DEFINITION for view tables
396430
LEFT JOIN INFORMATION_SCHEMA.VIEWS v
397431
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
401435
ORDER BY t.TABLE_NAME;
402436
`;
403437
}
404438

405439
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.
406443
return `
407444
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')")
411448
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
414451
ORDER BY c.TABLE_NAME, c.COLUMN_NAME;
415452
`;
416453
}

0 commit comments

Comments
 (0)