diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index 9f2fc8701..f33c8656f 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -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"), }), ); diff --git a/packages/db/test/schema.spec.ts b/packages/db/test/schema.spec.ts index 6606636e1..dd1e14df0 100644 --- a/packages/db/test/schema.spec.ts +++ b/packages/db/test/schema.spec.ts @@ -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().toEqualTypeOf(); expectTypeOf().toEqualTypeOf(); @@ -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().toEqualTypeOf(); expectTypeOf().toEqualTypeOf();