@zmdb/mysql 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -0
- package/README.md +111 -0
- package/dist/dialect.d.ts +2 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +91 -0
- package/dist/dialect.js.map +1 -0
- package/dist/driver.d.ts +48 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +125 -0
- package/dist/driver.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect.d.ts +11 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +306 -0
- package/dist/introspect.js.map +1 -0
- package/dist/migrations.d.ts +25 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +471 -0
- package/dist/migrations.js.map +1 -0
- package/package.json +59 -0
- package/src/__fixtures__/mysql-8.4.11.json +261 -0
- package/src/dialect.ts +99 -0
- package/src/driver.ts +219 -0
- package/src/index.ts +35 -0
- package/src/introspect.ts +430 -0
- package/src/migrations.ts +637 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import type { SchemaSnapshot } from '@zmdb/migrations';
|
|
2
|
+
import {
|
|
3
|
+
action,
|
|
4
|
+
CatalogRowError,
|
|
5
|
+
flagField,
|
|
6
|
+
integerField,
|
|
7
|
+
normalizeDriftSnapshot,
|
|
8
|
+
nullableIntegerField,
|
|
9
|
+
nullableTextField,
|
|
10
|
+
query,
|
|
11
|
+
sortByName,
|
|
12
|
+
sortWarnings,
|
|
13
|
+
tableSelected,
|
|
14
|
+
textField,
|
|
15
|
+
type CatalogColumnSnapshot,
|
|
16
|
+
type CatalogForeignKeySnapshot,
|
|
17
|
+
type CatalogIndexColumn,
|
|
18
|
+
type CatalogIndexSnapshot,
|
|
19
|
+
type CatalogSchemaSnapshot,
|
|
20
|
+
type CatalogTableSnapshot,
|
|
21
|
+
type CatalogWarning,
|
|
22
|
+
} from '@zmdb/migrations/introspect/runtime';
|
|
23
|
+
import { type IntrospectionDriver, type Introspector, type IntrospectOptions } from '@zmdb/sql';
|
|
24
|
+
|
|
25
|
+
interface MysqlColumn {
|
|
26
|
+
readonly table: string;
|
|
27
|
+
readonly ordinal: number;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly nullable: boolean;
|
|
30
|
+
readonly dataType: string;
|
|
31
|
+
readonly catalogType: string;
|
|
32
|
+
readonly length: number | null;
|
|
33
|
+
readonly default: string | null;
|
|
34
|
+
readonly extra: string;
|
|
35
|
+
readonly generationExpression: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface MysqlKeyColumn {
|
|
39
|
+
readonly table: string;
|
|
40
|
+
readonly constraint: string;
|
|
41
|
+
readonly ordinal: number;
|
|
42
|
+
readonly column: string;
|
|
43
|
+
readonly targetTable: string | null;
|
|
44
|
+
readonly targetColumn: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface MysqlReference {
|
|
48
|
+
readonly table: string;
|
|
49
|
+
readonly constraint: string;
|
|
50
|
+
readonly onUpdate: string;
|
|
51
|
+
readonly onDelete: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MysqlStatistic {
|
|
55
|
+
readonly table: string;
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly nonUnique: boolean;
|
|
58
|
+
readonly sequence: number;
|
|
59
|
+
readonly column: string | null;
|
|
60
|
+
readonly expression: string | null;
|
|
61
|
+
readonly method: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MysqlCatalogOverrides {
|
|
65
|
+
readonly snapshot?: (
|
|
66
|
+
driver: IntrospectionDriver,
|
|
67
|
+
options: IntrospectOptions,
|
|
68
|
+
parent: (driver: IntrospectionDriver, options?: IntrospectOptions) => Promise<CatalogSchemaSnapshot>,
|
|
69
|
+
) => Promise<CatalogSchemaSnapshot>;
|
|
70
|
+
readonly normalizeForDrift?: (
|
|
71
|
+
snapshot: SchemaSnapshot,
|
|
72
|
+
role: 'live' | 'declared',
|
|
73
|
+
parent: (snapshot: SchemaSnapshot, role: 'live' | 'declared') => SchemaSnapshot,
|
|
74
|
+
) => SchemaSnapshot;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function placeholders(count: number): string {
|
|
78
|
+
return Array.from({ length: count }, () => '?').join(', ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function schemaFilter(
|
|
82
|
+
field: string,
|
|
83
|
+
options: IntrospectOptions,
|
|
84
|
+
): {
|
|
85
|
+
readonly sql: string;
|
|
86
|
+
readonly parameters: readonly unknown[];
|
|
87
|
+
} {
|
|
88
|
+
const schemas = options.schemas;
|
|
89
|
+
if (schemas === undefined || schemas.length === 0) return { sql: `${field} = DATABASE()`, parameters: [] };
|
|
90
|
+
const distinct = [...new Set(schemas)].toSorted();
|
|
91
|
+
return { sql: `${field} IN (${placeholders(distinct.length)})`, parameters: distinct };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseColumn(row: Readonly<Record<string, unknown>>, index: number): MysqlColumn {
|
|
95
|
+
const catalog = 'mysql information_schema.COLUMNS';
|
|
96
|
+
const nullable = textField(row, 'IS_NULLABLE', catalog, index);
|
|
97
|
+
if (nullable !== 'YES' && nullable !== 'NO') {
|
|
98
|
+
throw new CatalogRowError(catalog, index, 'IS_NULLABLE', '"YES" or "NO"', nullable);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
table: textField(row, 'TABLE_NAME', catalog, index),
|
|
102
|
+
ordinal: integerField(row, 'ORDINAL_POSITION', catalog, index),
|
|
103
|
+
name: textField(row, 'COLUMN_NAME', catalog, index),
|
|
104
|
+
nullable: nullable === 'YES',
|
|
105
|
+
dataType: textField(row, 'DATA_TYPE', catalog, index),
|
|
106
|
+
catalogType: textField(row, 'COLUMN_TYPE', catalog, index),
|
|
107
|
+
length: nullableIntegerField(row, 'CHARACTER_MAXIMUM_LENGTH', catalog, index),
|
|
108
|
+
default: nullableTextField(row, 'COLUMN_DEFAULT', catalog, index),
|
|
109
|
+
extra: textField(row, 'EXTRA', catalog, index),
|
|
110
|
+
generationExpression: textField(row, 'GENERATION_EXPRESSION', catalog, index),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseKeyColumn(row: Readonly<Record<string, unknown>>, index: number): MysqlKeyColumn {
|
|
115
|
+
const catalog = 'mysql information_schema.KEY_COLUMN_USAGE';
|
|
116
|
+
return {
|
|
117
|
+
table: textField(row, 'TABLE_NAME', catalog, index),
|
|
118
|
+
constraint: textField(row, 'CONSTRAINT_NAME', catalog, index),
|
|
119
|
+
ordinal: integerField(row, 'ORDINAL_POSITION', catalog, index),
|
|
120
|
+
column: textField(row, 'COLUMN_NAME', catalog, index),
|
|
121
|
+
targetTable: nullableTextField(row, 'REFERENCED_TABLE_NAME', catalog, index),
|
|
122
|
+
targetColumn: nullableTextField(row, 'REFERENCED_COLUMN_NAME', catalog, index),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseReference(row: Readonly<Record<string, unknown>>, index: number): MysqlReference {
|
|
127
|
+
const catalog = 'mysql information_schema.REFERENTIAL_CONSTRAINTS';
|
|
128
|
+
return {
|
|
129
|
+
table: textField(row, 'TABLE_NAME', catalog, index),
|
|
130
|
+
constraint: textField(row, 'CONSTRAINT_NAME', catalog, index),
|
|
131
|
+
onUpdate: textField(row, 'UPDATE_RULE', catalog, index),
|
|
132
|
+
onDelete: textField(row, 'DELETE_RULE', catalog, index),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseStatistic(row: Readonly<Record<string, unknown>>, index: number): MysqlStatistic {
|
|
137
|
+
const catalog = 'mysql information_schema.STATISTICS';
|
|
138
|
+
return {
|
|
139
|
+
table: textField(row, 'TABLE_NAME', catalog, index),
|
|
140
|
+
name: textField(row, 'INDEX_NAME', catalog, index),
|
|
141
|
+
nonUnique: flagField(row, 'NON_UNIQUE', catalog, index),
|
|
142
|
+
sequence: integerField(row, 'SEQ_IN_INDEX', catalog, index),
|
|
143
|
+
column: nullableTextField(row, 'COLUMN_NAME', catalog, index),
|
|
144
|
+
expression: nullableTextField(row, 'EXPRESSION', catalog, index),
|
|
145
|
+
method: textField(row, 'INDEX_TYPE', catalog, index),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function mysqlType(column: MysqlColumn): {
|
|
150
|
+
readonly type: string;
|
|
151
|
+
readonly length?: number;
|
|
152
|
+
readonly warning?: string;
|
|
153
|
+
} {
|
|
154
|
+
const dataType = column.dataType.toLowerCase();
|
|
155
|
+
const catalogType = column.catalogType.toLowerCase();
|
|
156
|
+
if (column.extra.toLowerCase().split(/\s+/).includes('auto_increment')) return { type: 'serial' };
|
|
157
|
+
if (dataType === 'int' || dataType === 'integer' || dataType === 'mediumint') return { type: 'integer' };
|
|
158
|
+
if (dataType === 'bigint') return { type: 'bigint' };
|
|
159
|
+
if (dataType === 'tinyint' && /^tinyint\s*\(\s*1\s*\)/.test(catalogType)) return { type: 'boolean' };
|
|
160
|
+
if (dataType === 'tinyint' || dataType === 'smallint') {
|
|
161
|
+
return { type: 'integer', warning: `MySQL type ${column.catalogType} was widened to integer` };
|
|
162
|
+
}
|
|
163
|
+
if (dataType === 'decimal' || dataType === 'numeric') return { type: 'numeric' };
|
|
164
|
+
if (dataType === 'varchar') {
|
|
165
|
+
return column.length === null
|
|
166
|
+
? { type: 'varchar', warning: `MySQL varchar ${column.catalogType} reported no length` }
|
|
167
|
+
: { type: 'varchar', length: column.length };
|
|
168
|
+
}
|
|
169
|
+
if (dataType === 'text' || dataType === 'tinytext' || dataType === 'mediumtext' || dataType === 'longtext') {
|
|
170
|
+
return dataType === 'text'
|
|
171
|
+
? { type: 'text' }
|
|
172
|
+
: { type: 'text', warning: `MySQL type ${column.catalogType} was widened to text` };
|
|
173
|
+
}
|
|
174
|
+
if (dataType === 'datetime') {
|
|
175
|
+
return catalogType === 'datetime(3)'
|
|
176
|
+
? { type: 'timestamp' }
|
|
177
|
+
: {
|
|
178
|
+
type: 'timestamp',
|
|
179
|
+
warning: `MySQL type ${column.catalogType} was normalized to timestamp; forward DDL emits DATETIME(3)`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (dataType === 'timestamp') {
|
|
183
|
+
return {
|
|
184
|
+
type: 'timestamp',
|
|
185
|
+
warning:
|
|
186
|
+
'MySQL TIMESTAMP converts through the session time zone and has a 2038 limit; forward DDL emits DATETIME(3)',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (dataType === 'json') return { type: 'json' };
|
|
190
|
+
if (dataType === 'enum') return { type: 'jsonEnum' };
|
|
191
|
+
return {
|
|
192
|
+
type: column.catalogType,
|
|
193
|
+
warning: `MySQL type ${column.catalogType} cannot be represented by the current declared SQL type vocabulary`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function primaryKeys(keys: readonly MysqlKeyColumn[]): ReadonlyMap<string, readonly string[]> {
|
|
198
|
+
const grouped = new Map<string, MysqlKeyColumn[]>();
|
|
199
|
+
for (const key of keys) {
|
|
200
|
+
if (key.constraint !== 'PRIMARY') continue;
|
|
201
|
+
const values = grouped.get(key.table);
|
|
202
|
+
if (values) values.push(key);
|
|
203
|
+
else grouped.set(key.table, [key]);
|
|
204
|
+
}
|
|
205
|
+
return new Map(
|
|
206
|
+
[...grouped].map(([table, values]) => [
|
|
207
|
+
table,
|
|
208
|
+
values.toSorted((left, right) => left.ordinal - right.ordinal).map(value => value.column),
|
|
209
|
+
]),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function foreignKeys(
|
|
214
|
+
keys: readonly MysqlKeyColumn[],
|
|
215
|
+
references: readonly MysqlReference[],
|
|
216
|
+
): ReadonlyMap<string, readonly CatalogForeignKeySnapshot[]> {
|
|
217
|
+
const referenceByName = new Map(
|
|
218
|
+
references.map(reference => [`${reference.table}\0${reference.constraint}`, reference]),
|
|
219
|
+
);
|
|
220
|
+
const grouped = new Map<string, MysqlKeyColumn[]>();
|
|
221
|
+
for (const key of keys) {
|
|
222
|
+
if (key.targetTable === null || key.targetColumn === null) continue;
|
|
223
|
+
const id = `${key.table}\0${key.constraint}`;
|
|
224
|
+
const values = grouped.get(id);
|
|
225
|
+
if (values) values.push(key);
|
|
226
|
+
else grouped.set(id, [key]);
|
|
227
|
+
}
|
|
228
|
+
const result = new Map<string, CatalogForeignKeySnapshot[]>();
|
|
229
|
+
for (const [id, values] of grouped) {
|
|
230
|
+
values.sort((left, right) => left.ordinal - right.ordinal);
|
|
231
|
+
const first = values[0];
|
|
232
|
+
if (first === undefined) continue;
|
|
233
|
+
const reference = referenceByName.get(id);
|
|
234
|
+
if (reference === undefined) {
|
|
235
|
+
throw new TypeError(
|
|
236
|
+
`mysql catalog has key columns for foreign key "${first.constraint}" but no referential constraint row`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const targetTable = first.targetTable;
|
|
240
|
+
if (
|
|
241
|
+
targetTable === null ||
|
|
242
|
+
values.some(value => value.targetTable !== targetTable || value.targetColumn === null)
|
|
243
|
+
) {
|
|
244
|
+
throw new TypeError(`mysql foreign key "${first.constraint}" has inconsistent target columns`);
|
|
245
|
+
}
|
|
246
|
+
const snapshot: CatalogForeignKeySnapshot = {
|
|
247
|
+
name: first.constraint,
|
|
248
|
+
columns: values.map(value => value.column),
|
|
249
|
+
targetTable,
|
|
250
|
+
targetColumns: values.map(value => value.targetColumn).filter(value => value !== null),
|
|
251
|
+
onDelete: action(reference.onDelete, 'mysql information_schema.REFERENTIAL_CONSTRAINTS', 0, 'DELETE_RULE'),
|
|
252
|
+
onUpdate: action(reference.onUpdate, 'mysql information_schema.REFERENTIAL_CONSTRAINTS', 0, 'UPDATE_RULE'),
|
|
253
|
+
};
|
|
254
|
+
const tableValues = result.get(first.table);
|
|
255
|
+
if (tableValues) tableValues.push(snapshot);
|
|
256
|
+
else result.set(first.table, [snapshot]);
|
|
257
|
+
}
|
|
258
|
+
return new Map([...result].map(([table, values]) => [table, sortByName(values)]));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function indexes(statistics: readonly MysqlStatistic[]): ReadonlyMap<string, readonly CatalogIndexSnapshot[]> {
|
|
262
|
+
const grouped = new Map<string, MysqlStatistic[]>();
|
|
263
|
+
for (const statistic of statistics) {
|
|
264
|
+
if (statistic.name === 'PRIMARY') continue;
|
|
265
|
+
const id = `${statistic.table}\0${statistic.name}`;
|
|
266
|
+
const values = grouped.get(id);
|
|
267
|
+
if (values) values.push(statistic);
|
|
268
|
+
else grouped.set(id, [statistic]);
|
|
269
|
+
}
|
|
270
|
+
const byTable = new Map<string, CatalogIndexSnapshot[]>();
|
|
271
|
+
for (const values of grouped.values()) {
|
|
272
|
+
values.sort((left, right) => left.sequence - right.sequence);
|
|
273
|
+
const first = values[0];
|
|
274
|
+
if (first === undefined) continue;
|
|
275
|
+
const columns: CatalogIndexColumn[] = values.map(value => {
|
|
276
|
+
if (value.expression !== null) return { expr: value.expression };
|
|
277
|
+
if (value.column !== null) return value.column;
|
|
278
|
+
throw new TypeError(`mysql index "${first.name}" has neither COLUMN_NAME nor EXPRESSION`);
|
|
279
|
+
});
|
|
280
|
+
const method = first.method.toLowerCase();
|
|
281
|
+
const snapshot: CatalogIndexSnapshot = {
|
|
282
|
+
name: first.name,
|
|
283
|
+
columns,
|
|
284
|
+
unique: !first.nonUnique,
|
|
285
|
+
...(method === 'btree' ? {} : { method }),
|
|
286
|
+
};
|
|
287
|
+
const tableValues = byTable.get(first.table);
|
|
288
|
+
if (tableValues) tableValues.push(snapshot);
|
|
289
|
+
else byTable.set(first.table, [snapshot]);
|
|
290
|
+
}
|
|
291
|
+
return new Map([...byTable].map(([table, values]) => [table, sortByName(values)]));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function mysqlSnapshot(
|
|
295
|
+
driver: IntrospectionDriver,
|
|
296
|
+
options: IntrospectOptions = {},
|
|
297
|
+
): Promise<CatalogSchemaSnapshot> {
|
|
298
|
+
const tablesFilter = schemaFilter('TABLE_SCHEMA', options);
|
|
299
|
+
const constraintFilter = schemaFilter('CONSTRAINT_SCHEMA', options);
|
|
300
|
+
const [tableRows, columnRows, statisticRows, keyRows, referenceRows] = await Promise.all([
|
|
301
|
+
driver.execute(
|
|
302
|
+
query(
|
|
303
|
+
`SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, ENGINE FROM information_schema.TABLES ` +
|
|
304
|
+
`WHERE ${tablesFilter.sql} AND TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME`,
|
|
305
|
+
tablesFilter.parameters,
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
driver.execute(
|
|
309
|
+
query(
|
|
310
|
+
`SELECT TABLE_NAME, ORDINAL_POSITION, COLUMN_NAME, IS_NULLABLE, DATA_TYPE, COLUMN_TYPE, ` +
|
|
311
|
+
`CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, NUMERIC_SCALE, COLUMN_DEFAULT, EXTRA, ` +
|
|
312
|
+
`GENERATION_EXPRESSION FROM information_schema.COLUMNS WHERE ${tablesFilter.sql} ` +
|
|
313
|
+
`ORDER BY TABLE_NAME, ORDINAL_POSITION`,
|
|
314
|
+
tablesFilter.parameters,
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
|
+
driver.execute(
|
|
318
|
+
query(
|
|
319
|
+
`SELECT TABLE_NAME, INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, EXPRESSION, INDEX_TYPE ` +
|
|
320
|
+
`FROM information_schema.STATISTICS WHERE ${tablesFilter.sql} ` +
|
|
321
|
+
`ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`,
|
|
322
|
+
tablesFilter.parameters,
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
driver.execute(
|
|
326
|
+
query(
|
|
327
|
+
`SELECT TABLE_NAME, CONSTRAINT_NAME, ORDINAL_POSITION, POSITION_IN_UNIQUE_CONSTRAINT, ` +
|
|
328
|
+
`COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME ` +
|
|
329
|
+
`FROM information_schema.KEY_COLUMN_USAGE WHERE ${constraintFilter.sql} ` +
|
|
330
|
+
`ORDER BY TABLE_NAME, CONSTRAINT_NAME, ORDINAL_POSITION`,
|
|
331
|
+
constraintFilter.parameters,
|
|
332
|
+
),
|
|
333
|
+
),
|
|
334
|
+
driver.execute(
|
|
335
|
+
query(
|
|
336
|
+
`SELECT TABLE_NAME, CONSTRAINT_NAME, UNIQUE_CONSTRAINT_NAME, MATCH_OPTION, UPDATE_RULE, DELETE_RULE ` +
|
|
337
|
+
`FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE ${constraintFilter.sql} ` +
|
|
338
|
+
`ORDER BY TABLE_NAME, CONSTRAINT_NAME`,
|
|
339
|
+
constraintFilter.parameters,
|
|
340
|
+
),
|
|
341
|
+
),
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
const tableNames = tableRows.map((row, index) =>
|
|
345
|
+
textField(row, 'TABLE_NAME', 'mysql information_schema.TABLES', index),
|
|
346
|
+
);
|
|
347
|
+
const duplicate = tableNames.find((name, index) => tableNames.indexOf(name) !== index);
|
|
348
|
+
if (duplicate !== undefined) {
|
|
349
|
+
throw new TypeError(
|
|
350
|
+
`mysql introspection cannot represent table "${duplicate}" from more than one schema in a schema-neutral snapshot`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const parsedColumns = columnRows.map(parseColumn);
|
|
355
|
+
const parsedKeys = keyRows.map(parseKeyColumn);
|
|
356
|
+
const parsedReferences = referenceRows.map(parseReference);
|
|
357
|
+
const parsedStatistics = statisticRows.map(parseStatistic);
|
|
358
|
+
const keyByTable = primaryKeys(parsedKeys);
|
|
359
|
+
const foreignKeyByTable = foreignKeys(parsedKeys, parsedReferences);
|
|
360
|
+
const indexByTable = indexes(parsedStatistics);
|
|
361
|
+
|
|
362
|
+
const tables: CatalogTableSnapshot[] = [];
|
|
363
|
+
const warnings: CatalogWarning[] = [];
|
|
364
|
+
for (const name of tableNames) {
|
|
365
|
+
if (!tableSelected(name, options)) continue;
|
|
366
|
+
const primaryKey = keyByTable.get(name) ?? [];
|
|
367
|
+
const columns: CatalogColumnSnapshot[] = parsedColumns
|
|
368
|
+
.filter(column => column.table === name)
|
|
369
|
+
.map(column => {
|
|
370
|
+
const mapped = mysqlType(column);
|
|
371
|
+
if (mapped.warning !== undefined) {
|
|
372
|
+
warnings.push({ table: name, column: column.name, reason: mapped.warning });
|
|
373
|
+
}
|
|
374
|
+
const generated =
|
|
375
|
+
column.generationExpression.length === 0
|
|
376
|
+
? undefined
|
|
377
|
+
: {
|
|
378
|
+
expression: column.generationExpression,
|
|
379
|
+
stored: column.extra.toLowerCase().includes('stored generated'),
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
name: column.name,
|
|
383
|
+
type: mapped.type,
|
|
384
|
+
catalogType: column.catalogType,
|
|
385
|
+
nullable: column.nullable,
|
|
386
|
+
primaryKey: primaryKey.includes(column.name),
|
|
387
|
+
...(mapped.length === undefined ? {} : { length: mapped.length }),
|
|
388
|
+
...(column.default === null ? {} : { default: column.default }),
|
|
389
|
+
...(generated === undefined ? {} : { generated }),
|
|
390
|
+
};
|
|
391
|
+
})
|
|
392
|
+
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
393
|
+
tables.push({
|
|
394
|
+
name,
|
|
395
|
+
columns,
|
|
396
|
+
primaryKey,
|
|
397
|
+
foreignKeys: foreignKeyByTable.get(name) ?? [],
|
|
398
|
+
indexes: indexByTable.get(name) ?? [],
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
version: 1,
|
|
404
|
+
tables: sortByName(tables),
|
|
405
|
+
extensions: [],
|
|
406
|
+
warnings: sortWarnings(warnings),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function mysqlNormalize(snapshot: SchemaSnapshot, role: 'live' | 'declared'): SchemaSnapshot {
|
|
411
|
+
return normalizeDriftSnapshot(snapshot, role, { omitForeignKeySupportIndexes: true });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function mysqlFamilyIntrospector<Name extends string>(
|
|
415
|
+
name: Name,
|
|
416
|
+
overrides: MysqlCatalogOverrides = {},
|
|
417
|
+
): Introspector<Name> {
|
|
418
|
+
const snapshot = overrides.snapshot;
|
|
419
|
+
const normalize = overrides.normalizeForDrift;
|
|
420
|
+
const introspector: Introspector<Name> = {
|
|
421
|
+
name,
|
|
422
|
+
snapshot: (driver, options = {}) =>
|
|
423
|
+
snapshot === undefined ? mysqlSnapshot(driver, options) : snapshot(driver, options, mysqlSnapshot),
|
|
424
|
+
normalizeForDrift: (catalog, role) =>
|
|
425
|
+
normalize === undefined ? mysqlNormalize(catalog, role) : normalize(catalog, role, mysqlNormalize),
|
|
426
|
+
};
|
|
427
|
+
return Object.freeze(introspector);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export const mysqlIntrospector: Introspector<'mysql'> = mysqlFamilyIntrospector('mysql');
|