fix(db): name the custom widget secret foreign key within mysql limits
drizzle derives `custom_widget_secret_definition_id_custom_widget_definition_id_fk`, which is 65 characters. MySQL caps identifiers at 64 and rejects it with error 1059, so migration 0040 created the key under a shortened name. The schema never declared that name, so every `migration:mysql:generate` run re-proposed dropping and recreating the key under the long one, producing a migration that cannot be applied. Declaring the shortened name explicitly makes the schema match what is actually in the database. Only the mysql schema changes; sqlite and postgresql keep their derived names and need no migration. The parity test matched keys across dialects by generated name, which no longer holds, so it now matches on what a key links - its columns, target table and target columns - and compares the referential actions. That also means it reads keys through getTableConfig and so covers table-level declarations, not just inline .references() ones. Adds a guard test asserting no mysql table, foreign key or index name exceeds 64 characters, so the next long pairing fails in CI instead of producing an unappliable migration.
This commit is contained in:
2 files changed
+68
-35
No files matched your search
@@ -6,6 +6,7 @@ import type { AnyMySqlColumn } from "drizzle-orm/mysql-core";
|
||||
import {
|
||||
boolean,
|
||||
customType,
|
||||
foreignKey,
|
||||
index,
|
||||
int,
|
||||
mediumtext,
|
||||
@@ -597,14 +598,20 @@ export const customWidgetSecrets = mysqlTable(
|
||||
updatedAt: timestamp()
|
||||
.$onUpdateFn(() => new Date())
|
||||
.notNull(),
|
||||
definitionId: varchar({ length: 64 })
|
||||
.notNull()
|
||||
.references(() => customWidgetDefinitions.id, { onDelete: "cascade" }),
|
||||
definitionId: varchar({ length: 64 }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
compoundKey: primaryKey({
|
||||
columns: [table.definitionId, table.kind],
|
||||
}),
|
||||
// The name drizzle derives here is 65 characters, one over MySQL's 64-character identifier
|
||||
// limit, so migration 0040 created the key under this shortened name. Declaring it explicitly
|
||||
// keeps `drizzle-kit generate` from re-proposing an ALTER that MySQL rejects with error 1059.
|
||||
definitionReference: foreignKey({
|
||||
name: "cw_secret_definition_id_cw_definition_id_fk",
|
||||
columns: [table.definitionId],
|
||||
foreignColumns: [customWidgetDefinitions.id],
|
||||
}).onDelete("cascade"),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { is } from "drizzle-orm";
|
||||
import type { Column, InferSelectModel } from "drizzle-orm";
|
||||
import { MySqlTable, getTableConfig as getMysqlTableConfig } from "drizzle-orm/mysql-core";
|
||||
import type { ForeignKey as MysqlForeignKey, MySqlTableWithColumns } from "drizzle-orm/mysql-core";
|
||||
import type { PgTableWithColumns, ForeignKey as PostgresqlForeignKey } from "drizzle-orm/pg-core";
|
||||
import { SQLiteTable, getTableConfig as getSqliteTableConfig } from "drizzle-orm/sqlite-core";
|
||||
import type { ForeignKey as SqliteForeignKey, SQLiteTableWithColumns } from "drizzle-orm/sqlite-core";
|
||||
import { expect, expectTypeOf, test } from "vitest";
|
||||
|
||||
@@ -54,6 +57,21 @@ type FixedSqliteConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
/** Identifies a foreign key by what it links rather than by its generated name. */
|
||||
const referenceSignature = (foreignKey: {
|
||||
reference: () => { columns: { name: string }[]; foreignTable: object; foreignColumns: { name: string }[] };
|
||||
}) => {
|
||||
const reference = foreignKey.reference();
|
||||
const target = Object.keys(reference.foreignTable).sort().join("+");
|
||||
return `${reference.columns
|
||||
.map((column) => column.name)
|
||||
.sort()
|
||||
.join(",")}->${target}(${reference.foreignColumns
|
||||
.map((column) => column.name)
|
||||
.sort()
|
||||
.join(",")})`;
|
||||
};
|
||||
|
||||
test("schemas should match", () => {
|
||||
expectTypeOf<SqliteTables>().toEqualTypeOf<MysqlTables>();
|
||||
expectTypeOf<MysqlTables>().toEqualTypeOf<SqliteTables>();
|
||||
@@ -88,56 +106,64 @@ test("schemas should match", () => {
|
||||
});
|
||||
|
||||
const mysqlTable = mysqlTables[tableName];
|
||||
const sqliteForeignKeys = sqliteTable[Symbol.for("drizzle:SQLiteInlineForeignKeys") as keyof typeof sqliteTable] as
|
||||
| SqliteForeignKey[]
|
||||
| undefined;
|
||||
const mysqlForeignKeys = mysqlTable[Symbol.for("drizzle:MySqlInlineForeignKeys") as keyof typeof mysqlTable] as
|
||||
| MysqlForeignKey[]
|
||||
| undefined;
|
||||
// Read through getTableConfig so keys declared at table level count too, not just inline
|
||||
// `.references()` ones. MySQL needs a table-level declaration wherever drizzle's derived name
|
||||
// would exceed its 64-character identifier limit.
|
||||
if (!is(sqliteTable, SQLiteTable) || !is(mysqlTable, MySqlTable)) return;
|
||||
|
||||
if (!sqliteForeignKeys && !mysqlForeignKeys) return;
|
||||
const sqliteForeignKeys = getSqliteTableConfig(sqliteTable).foreignKeys as SqliteForeignKey[];
|
||||
const mysqlForeignKeys = getMysqlTableConfig(mysqlTable).foreignKeys as MysqlForeignKey[];
|
||||
|
||||
expect(mysqlForeignKeys, `mysql foreign key for ${tableName} to be defined`).toBeDefined();
|
||||
expect(sqliteForeignKeys, `sqlite foreign key for ${tableName} to be defined`).toBeDefined();
|
||||
if (sqliteForeignKeys.length === 0 && mysqlForeignKeys.length === 0) return;
|
||||
|
||||
expect(
|
||||
sqliteForeignKeys!.length,
|
||||
sqliteForeignKeys.length,
|
||||
`expect number of foreign keys in table ${tableName} to be the same for both schemas`,
|
||||
).toEqual(mysqlForeignKeys!.length);
|
||||
).toEqual(mysqlForeignKeys.length);
|
||||
|
||||
sqliteForeignKeys?.forEach((sqliteForeignKey) => {
|
||||
sqliteForeignKey.getName();
|
||||
const mysqlForeignKey = mysqlForeignKeys!.find((key) => key.getName() === sqliteForeignKey.getName());
|
||||
expect(
|
||||
mysqlForeignKey,
|
||||
`expect foreign key ${sqliteForeignKey.getName()} to be defined in mysql schema`,
|
||||
).toBeDefined();
|
||||
sqliteForeignKeys.forEach((sqliteForeignKey) => {
|
||||
const describeKey = `${tableName}.${referenceSignature(sqliteForeignKey)}`;
|
||||
// Matched on what the key actually does rather than on its generated name, because the name
|
||||
// is allowed to differ per dialect.
|
||||
const mysqlForeignKey = mysqlForeignKeys.find(
|
||||
(key) => referenceSignature(key) === referenceSignature(sqliteForeignKey),
|
||||
);
|
||||
expect(mysqlForeignKey, `expect foreign key ${describeKey} to be defined in mysql schema`).toBeDefined();
|
||||
|
||||
expect(
|
||||
sqliteForeignKey.onDelete,
|
||||
`expect foreign key (${sqliteForeignKey.getName()}) onDelete to be the same for both schemas`,
|
||||
`expect foreign key (${describeKey}) onDelete to be the same for both schemas`,
|
||||
).toEqual(mysqlForeignKey!.onDelete);
|
||||
|
||||
expect(
|
||||
sqliteForeignKey.onUpdate,
|
||||
`expect foreign key (${sqliteForeignKey.getName()}) onUpdate to be the same for both schemas`,
|
||||
`expect foreign key (${describeKey}) onUpdate to be the same for both schemas`,
|
||||
).toEqual(mysqlForeignKey!.onUpdate);
|
||||
|
||||
sqliteForeignKey.reference().foreignColumns.forEach((column) => {
|
||||
expect(
|
||||
mysqlForeignKey!.reference().foreignColumns.map((column) => column.name),
|
||||
`expect foreign key (${sqliteForeignKey.getName()}) columns to be the same for both schemas`,
|
||||
).toContainEqual(column.name);
|
||||
});
|
||||
|
||||
expect(
|
||||
Object.keys(sqliteForeignKey.reference().foreignTable),
|
||||
`expect foreign key (${sqliteForeignKey.getName()}) table to be the same for both schemas`,
|
||||
).toEqual(Object.keys(mysqlForeignKey!.reference().foreignTable));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("mysql identifiers stay within the 64 character limit", () => {
|
||||
// MySQL rejects longer identifiers with error 1059, and drizzle derives foreign key names from
|
||||
// the table and column names, so a long pairing silently produces a migration that cannot apply.
|
||||
const tooLong: string[] = [];
|
||||
|
||||
objectEntries(mysqlSchema).forEach(([, table]) => {
|
||||
if (!is(table, MySqlTable)) return;
|
||||
|
||||
const config = getMysqlTableConfig(table);
|
||||
if (config.name.length > 64) tooLong.push(config.name);
|
||||
for (const foreignKey of config.foreignKeys) {
|
||||
if (foreignKey.getName().length > 64) tooLong.push(foreignKey.getName());
|
||||
}
|
||||
for (const index of config.indexes) {
|
||||
if (index.config.name.length > 64) tooLong.push(index.config.name);
|
||||
}
|
||||
});
|
||||
|
||||
expect(tooLong).toEqual([]);
|
||||
});
|
||||
|
||||
test("schemas should match for postgresql", () => {
|
||||
expectTypeOf<SqliteTables>().toEqualTypeOf<PostgresqlTables>();
|
||||
expectTypeOf<PostgresqlTables>().toEqualTypeOf<SqliteTables>();
|
||||
|
||||
Reference in new issue
Block a user