forked from boos/BoosAPI
fix(db): handle mysql information schema field casing
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { normalizeDefaultValue, normalizeSqlType } from './schemaIntrospection.js';
|
import { normalizeDefaultValue, normalizeSqlType, readMySqlField } from './schemaIntrospection.js';
|
||||||
|
|
||||||
describe('schema introspection normalization', () => {
|
describe('schema introspection normalization', () => {
|
||||||
it('normalizes booleans consistently across dialects', () => {
|
it('normalizes booleans consistently across dialects', () => {
|
||||||
@@ -13,4 +13,10 @@ describe('schema introspection normalization', () => {
|
|||||||
expect(normalizeDefaultValue('DEFAULT FALSE')).toBe('false');
|
expect(normalizeDefaultValue('DEFAULT FALSE')).toBe('false');
|
||||||
expect(normalizeDefaultValue("datetime('now')")).toBe("datetime('now')");
|
expect(normalizeDefaultValue("datetime('now')")).toBe("datetime('now')");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reads mysql information_schema fields regardless of casing', () => {
|
||||||
|
expect(readMySqlField({ COLUMN_TYPE: 'varchar(191)' }, 'column_type')).toBe('varchar(191)');
|
||||||
|
expect(readMySqlField({ column_type: 'text' }, 'column_type')).toBe('text');
|
||||||
|
expect(readMySqlField({ Table_Name: 'settings' }, 'table_name')).toBe('settings');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,6 +122,20 @@ type PostgresForeignKeyRow = {
|
|||||||
delete_rule: string | null;
|
delete_rule: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function readMySqlField<T>(row: Record<string, unknown>, field: string): T | undefined {
|
||||||
|
const exact = row[field];
|
||||||
|
if (exact !== undefined) return exact as T;
|
||||||
|
|
||||||
|
const upper = row[field.toUpperCase()];
|
||||||
|
if (upper !== undefined) return upper as T;
|
||||||
|
|
||||||
|
const lower = row[field.toLowerCase()];
|
||||||
|
if (lower !== undefined) return lower as T;
|
||||||
|
|
||||||
|
const matchedKey = Object.keys(row).find((key) => key.toLowerCase() === field.toLowerCase());
|
||||||
|
return matchedKey ? row[matchedKey] as T : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function splitMigrationStatements(sqlText: string): string[] {
|
function splitMigrationStatements(sqlText: string): string[] {
|
||||||
return sqlText
|
return sqlText
|
||||||
.split('--> statement-breakpoint')
|
.split('--> statement-breakpoint')
|
||||||
@@ -452,16 +466,20 @@ async function introspectMySqlSchema(input: SchemaIntrospectionInput): Promise<S
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [tableRows] = await connection.query(`
|
const [tableRows] = await connection.query(`
|
||||||
SELECT table_name
|
SELECT table_name AS table_name
|
||||||
FROM information_schema.tables
|
FROM information_schema.tables
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
AND table_type = 'BASE TABLE'
|
AND table_type = 'BASE TABLE'
|
||||||
ORDER BY table_name ASC
|
ORDER BY table_name ASC
|
||||||
`);
|
`);
|
||||||
const tableNames = (tableRows as Array<{ table_name: string }>).map((row) => row.table_name);
|
const tableNames = (tableRows as Array<Record<string, unknown>>)
|
||||||
|
.map((row) => readMySqlField<string>(row, 'table_name'))
|
||||||
|
.filter((tableName): tableName is string => typeof tableName === 'string');
|
||||||
|
|
||||||
const [primaryKeyRows] = await connection.query(`
|
const [primaryKeyRows] = await connection.query(`
|
||||||
SELECT kcu.table_name, kcu.column_name
|
SELECT
|
||||||
|
kcu.table_name AS table_name,
|
||||||
|
kcu.column_name AS column_name
|
||||||
FROM information_schema.table_constraints tc
|
FROM information_schema.table_constraints tc
|
||||||
JOIN information_schema.key_column_usage kcu
|
JOIN information_schema.key_column_usage kcu
|
||||||
ON tc.constraint_schema = kcu.constraint_schema
|
ON tc.constraint_schema = kcu.constraint_schema
|
||||||
@@ -470,10 +488,20 @@ async function introspectMySqlSchema(input: SchemaIntrospectionInput): Promise<S
|
|||||||
WHERE tc.table_schema = DATABASE()
|
WHERE tc.table_schema = DATABASE()
|
||||||
AND tc.constraint_type = 'PRIMARY KEY'
|
AND tc.constraint_type = 'PRIMARY KEY'
|
||||||
`);
|
`);
|
||||||
const primaryKeys = new Set((primaryKeyRows as MySqlPrimaryKeyRow[]).map((row) => `${row.table_name}.${row.column_name}`));
|
const primaryKeys = new Set((primaryKeyRows as Array<Record<string, unknown>>).map((row) => {
|
||||||
|
const tableName = readMySqlField<string>(row, 'table_name');
|
||||||
|
const columnName = readMySqlField<string>(row, 'column_name');
|
||||||
|
return tableName && columnName ? `${tableName}.${columnName}` : null;
|
||||||
|
}).filter((value): value is string => value !== null));
|
||||||
|
|
||||||
const [columnRows] = await connection.query(`
|
const [columnRows] = await connection.query(`
|
||||||
SELECT table_name, column_name, data_type, column_type, is_nullable, column_default
|
SELECT
|
||||||
|
table_name AS table_name,
|
||||||
|
column_name AS column_name,
|
||||||
|
data_type AS data_type,
|
||||||
|
column_type AS column_type,
|
||||||
|
is_nullable AS is_nullable,
|
||||||
|
column_default AS column_default
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
ORDER BY table_name ASC, ordinal_position ASC
|
ORDER BY table_name ASC, ordinal_position ASC
|
||||||
@@ -484,40 +512,61 @@ async function introspectMySqlSchema(input: SchemaIntrospectionInput): Promise<S
|
|||||||
tableMap.set(tableName, { columns: {} });
|
tableMap.set(tableName, { columns: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const row of columnRows as MySqlColumnRow[]) {
|
for (const row of columnRows as Array<Record<string, unknown>>) {
|
||||||
const logicalType = normalizeSqlType('mysql', row.column_type || row.data_type, row.column_name, row.column_default);
|
const tableName = readMySqlField<string>(row, 'table_name');
|
||||||
tableMap.get(row.table_name)!.columns[row.column_name] = {
|
const columnName = readMySqlField<string>(row, 'column_name');
|
||||||
|
const declaredType = readMySqlField<string>(row, 'column_type') || readMySqlField<string>(row, 'data_type');
|
||||||
|
const isNullable = readMySqlField<string>(row, 'is_nullable');
|
||||||
|
const columnDefault = readMySqlField<string | null>(row, 'column_default') ?? null;
|
||||||
|
if (!tableName || !columnName || !declaredType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logicalType = normalizeSqlType('mysql', declaredType, columnName, columnDefault);
|
||||||
|
tableMap.get(tableName)!.columns[columnName] = {
|
||||||
logicalType,
|
logicalType,
|
||||||
notNull: row.is_nullable === 'NO',
|
notNull: isNullable === 'NO',
|
||||||
defaultValue: primaryKeys.has(`${row.table_name}.${row.column_name}`)
|
defaultValue: primaryKeys.has(`${tableName}.${columnName}`)
|
||||||
? null
|
? null
|
||||||
: normalizeDefaultValueForColumn(row.column_default, logicalType),
|
: normalizeDefaultValueForColumn(columnDefault, logicalType),
|
||||||
primaryKey: primaryKeys.has(`${row.table_name}.${row.column_name}`),
|
primaryKey: primaryKeys.has(`${tableName}.${columnName}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const tables = Object.fromEntries(tableNames.map((tableName) => [tableName, tableMap.get(tableName)!]));
|
const tables = Object.fromEntries(tableNames.map((tableName) => [tableName, tableMap.get(tableName)!]));
|
||||||
|
|
||||||
const [indexRows] = await connection.query(`
|
const [indexRows] = await connection.query(`
|
||||||
SELECT table_name, index_name, column_name, non_unique
|
SELECT
|
||||||
|
table_name AS table_name,
|
||||||
|
index_name AS index_name,
|
||||||
|
column_name AS column_name,
|
||||||
|
non_unique AS non_unique
|
||||||
FROM information_schema.statistics
|
FROM information_schema.statistics
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
AND index_name <> 'PRIMARY'
|
AND index_name <> 'PRIMARY'
|
||||||
ORDER BY table_name ASC, index_name ASC, seq_in_index ASC
|
ORDER BY table_name ASC, index_name ASC, seq_in_index ASC
|
||||||
`);
|
`);
|
||||||
const indexGroups = new Map<string, SchemaContractIndex>();
|
const indexGroups = new Map<string, SchemaContractIndex>();
|
||||||
for (const row of indexRows as MySqlIndexRow[]) {
|
for (const row of indexRows as Array<Record<string, unknown>>) {
|
||||||
const key = `${row.table_name}:${row.index_name}`;
|
const tableName = readMySqlField<string>(row, 'table_name');
|
||||||
|
const indexName = readMySqlField<string>(row, 'index_name');
|
||||||
|
const columnName = readMySqlField<string>(row, 'column_name');
|
||||||
|
const nonUnique = readMySqlField<number>(row, 'non_unique');
|
||||||
|
if (!tableName || !indexName || !columnName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${tableName}:${indexName}`;
|
||||||
const existing = indexGroups.get(key);
|
const existing = indexGroups.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.columns.push(row.column_name);
|
existing.columns.push(columnName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
indexGroups.set(key, {
|
indexGroups.set(key, {
|
||||||
name: row.index_name,
|
name: indexName,
|
||||||
table: row.table_name,
|
table: tableName,
|
||||||
columns: [row.column_name],
|
columns: [columnName],
|
||||||
unique: Number(row.non_unique) === 0,
|
unique: Number(nonUnique) === 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,12 +578,12 @@ async function introspectMySqlSchema(input: SchemaIntrospectionInput): Promise<S
|
|||||||
|
|
||||||
const [foreignKeyRows] = await connection.query(`
|
const [foreignKeyRows] = await connection.query(`
|
||||||
SELECT
|
SELECT
|
||||||
kcu.table_name,
|
kcu.table_name AS table_name,
|
||||||
kcu.constraint_name,
|
kcu.constraint_name AS constraint_name,
|
||||||
kcu.column_name,
|
kcu.column_name AS column_name,
|
||||||
kcu.referenced_table_name,
|
kcu.referenced_table_name AS referenced_table_name,
|
||||||
kcu.referenced_column_name,
|
kcu.referenced_column_name AS referenced_column_name,
|
||||||
rc.delete_rule
|
rc.delete_rule AS delete_rule
|
||||||
FROM information_schema.key_column_usage kcu
|
FROM information_schema.key_column_usage kcu
|
||||||
JOIN information_schema.referential_constraints rc
|
JOIN information_schema.referential_constraints rc
|
||||||
ON rc.constraint_schema = kcu.constraint_schema
|
ON rc.constraint_schema = kcu.constraint_schema
|
||||||
@@ -544,20 +593,30 @@ async function introspectMySqlSchema(input: SchemaIntrospectionInput): Promise<S
|
|||||||
ORDER BY kcu.table_name ASC, kcu.constraint_name ASC, kcu.ordinal_position ASC
|
ORDER BY kcu.table_name ASC, kcu.constraint_name ASC, kcu.ordinal_position ASC
|
||||||
`);
|
`);
|
||||||
const foreignKeyGroups = new Map<string, SchemaContractForeignKey>();
|
const foreignKeyGroups = new Map<string, SchemaContractForeignKey>();
|
||||||
for (const row of foreignKeyRows as MySqlForeignKeyRow[]) {
|
for (const row of foreignKeyRows as Array<Record<string, unknown>>) {
|
||||||
const key = `${row.table_name}:${row.constraint_name}`;
|
const tableName = readMySqlField<string>(row, 'table_name');
|
||||||
|
const constraintName = readMySqlField<string>(row, 'constraint_name');
|
||||||
|
const columnName = readMySqlField<string>(row, 'column_name');
|
||||||
|
const referencedTableName = readMySqlField<string>(row, 'referenced_table_name');
|
||||||
|
const referencedColumnName = readMySqlField<string>(row, 'referenced_column_name');
|
||||||
|
const deleteRule = readMySqlField<string | null>(row, 'delete_rule') ?? null;
|
||||||
|
if (!tableName || !constraintName || !columnName || !referencedTableName || !referencedColumnName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `${tableName}:${constraintName}`;
|
||||||
const existing = foreignKeyGroups.get(key);
|
const existing = foreignKeyGroups.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.columns.push(row.column_name);
|
existing.columns.push(columnName);
|
||||||
existing.referencedColumns.push(row.referenced_column_name);
|
existing.referencedColumns.push(referencedColumnName);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
foreignKeyGroups.set(key, {
|
foreignKeyGroups.set(key, {
|
||||||
table: row.table_name,
|
table: tableName,
|
||||||
columns: [row.column_name],
|
columns: [columnName],
|
||||||
referencedTable: row.referenced_table_name,
|
referencedTable: referencedTableName,
|
||||||
referencedColumns: [row.referenced_column_name],
|
referencedColumns: [referencedColumnName],
|
||||||
onDelete: normalizeDeleteRule(row.delete_rule),
|
onDelete: normalizeDeleteRule(deleteRule),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -736,13 +795,17 @@ function readBootstrapSql(dialect: Exclude<SchemaIntrospectionDialect, 'sqlite'>
|
|||||||
async function resetMySqlSchema(connection: mysql.Connection): Promise<void> {
|
async function resetMySqlSchema(connection: mysql.Connection): Promise<void> {
|
||||||
await connection.query('SET FOREIGN_KEY_CHECKS = 0');
|
await connection.query('SET FOREIGN_KEY_CHECKS = 0');
|
||||||
const [rows] = await connection.query(`
|
const [rows] = await connection.query(`
|
||||||
SELECT table_name
|
SELECT table_name AS table_name
|
||||||
FROM information_schema.tables
|
FROM information_schema.tables
|
||||||
WHERE table_schema = DATABASE()
|
WHERE table_schema = DATABASE()
|
||||||
AND table_type = 'BASE TABLE'
|
AND table_type = 'BASE TABLE'
|
||||||
`);
|
`);
|
||||||
for (const row of rows as Array<{ table_name: string }>) {
|
for (const row of rows as Array<Record<string, unknown>>) {
|
||||||
await connection.query(`DROP TABLE IF EXISTS \`${row.table_name}\``);
|
const tableName = readMySqlField<string>(row, 'table_name');
|
||||||
|
if (!tableName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await connection.query(`DROP TABLE IF EXISTS \`${tableName}\``);
|
||||||
}
|
}
|
||||||
await connection.query('SET FOREIGN_KEY_CHECKS = 1');
|
await connection.query('SET FOREIGN_KEY_CHECKS = 1');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user