diff --git a/packages/better-auth/src/db/get-migration.ts b/packages/better-auth/src/db/get-migration.ts index 5f62d45c1f..57313eb187 100644 --- a/packages/better-auth/src/db/get-migration.ts +++ b/packages/better-auth/src/db/get-migration.ts @@ -137,13 +137,10 @@ interface DatabaseIndexDefinition { } interface DatabaseColumnRow { - characterMaximumLength?: number | string | null; - CHARACTER_MAXIMUM_LENGTH?: number | string | null; columnName?: string; COLUMN_NAME?: string; - dataType?: string; - DATA_TYPE?: string; - maxLength?: number | string; + maxIndexBytes?: number | string | null; + MAX_INDEX_BYTES?: number | string | null; tableName?: string; TABLE_NAME?: string; } @@ -152,6 +149,12 @@ interface DatabaseColumnBound { maxIndexBytes: number | null; } +type MigrationTarget< + DatabaseType extends KyselyDatabaseType = KyselyDatabaseType, +> = DatabaseType extends "postgres" | "mssql" + ? { type: DatabaseType; schema: string } + : { type: DatabaseType }; + function createDatabaseIndexKey(tableName: string, indexName: string) { return `${getPortableDatabaseIdentifierKey(tableName)}\u0000${getPortableDatabaseIdentifierKey(indexName)}`; } @@ -210,8 +213,7 @@ function toDatabaseIndexMap(indexes: readonly DatabaseIndexMetadata[]) { async function getDatabaseIndexMap( db: Kysely, - dbType: KyselyDatabaseType, - schemaName: string, + target: MigrationTarget, tableNames: readonly string[], introspectIndexes: DatabaseIndexIntrospector | undefined, ) { @@ -221,9 +223,10 @@ async function getDatabaseIndexMap( } let rows: readonly DatabaseIndexRow[]; - if (dbType === "sqlite") { - rows = ( - await sql` + switch (target.type) { + case "sqlite": + rows = ( + await sql` SELECT tables.name AS "tableName", index_list.name AS "indexName", @@ -236,10 +239,11 @@ async function getDatabaseIndexMap( INNER JOIN pragma_index_info(index_list.name) AS index_info WHERE tables.type = 'table' `.execute(db) - ).rows; - } else if (dbType === "postgres") { - rows = ( - await sql` + ).rows; + break; + case "postgres": + rows = ( + await sql` SELECT table_class.relname AS "tableName", index_class.relname AS "indexName", @@ -261,14 +265,15 @@ async function getDatabaseIndexMap( LEFT JOIN pg_attribute AS index_attribute ON index_attribute.attrelid = table_class.oid AND index_attribute.attnum = index_column.attribute_number - WHERE table_namespace.nspname = ${schemaName} + WHERE table_namespace.nspname = ${target.schema} AND table_class.relkind = 'r' AND index_column.ordinality <= index_data.indnkeyatts `.execute(db) - ).rows; - } else if (dbType === "mysql") { - rows = ( - await sql` + ).rows; + break; + case "mysql": + rows = ( + await sql` SELECT table_name AS tableName, index_name AS indexName, @@ -280,10 +285,11 @@ async function getDatabaseIndexMap( FROM information_schema.statistics WHERE table_schema = DATABASE() `.execute(db) - ).rows; - } else { - rows = ( - await sql` + ).rows; + break; + case "mssql": + rows = ( + await sql` SELECT tables.name AS "tableName", indexes.name AS "indexName", @@ -304,11 +310,12 @@ async function getDatabaseIndexMap( INNER JOIN sys.columns AS columns ON columns.object_id = index_columns.object_id AND columns.column_id = index_columns.column_id - WHERE table_schemas.name = ${schemaName} + WHERE table_schemas.name = ${target.schema} AND indexes.name IS NOT NULL AND index_columns.key_ordinal > 0 `.execute(db) - ).rows; + ).rows; + break; } const indexMetadata = new Map(); @@ -379,74 +386,57 @@ async function getDatabaseIndexMap( async function getDatabaseColumnBounds( db: Kysely, - dbType: KyselyDatabaseType, - schemaName: string, + target: MigrationTarget, ) { - if (dbType !== "mysql" && dbType !== "mssql") { - return new Map(); - } - let rows: readonly DatabaseColumnRow[]; - if (dbType === "mysql") { - rows = ( - await sql` + switch (target.type) { + case "postgres": + case "sqlite": + return new Map(); + case "mysql": + rows = ( + await sql` SELECT table_name AS tableName, column_name AS columnName, - data_type AS dataType, - character_maximum_length AS characterMaximumLength + character_maximum_length * 4 AS maxIndexBytes FROM information_schema.columns WHERE table_schema = DATABASE() `.execute(db) - ).rows; - } else { - rows = ( - await sql` + ).rows; + break; + case "mssql": + rows = ( + await sql` SELECT tables.name AS "tableName", columns.name AS "columnName", - types.name AS "dataType", - columns.max_length AS "maxLength" + columns.max_length AS "maxIndexBytes" FROM sys.columns AS columns INNER JOIN sys.tables AS tables ON tables.object_id = columns.object_id INNER JOIN sys.schemas AS table_schemas ON table_schemas.schema_id = tables.schema_id - INNER JOIN sys.types AS types - ON types.user_type_id = columns.user_type_id - WHERE table_schemas.name = ${schemaName} + WHERE table_schemas.name = ${target.schema} `.execute(db) - ).rows; + ).rows; + break; } - return new Map( - rows.flatMap((row) => { - const table = row.tableName ?? row.TABLE_NAME; - const column = row.columnName ?? row.COLUMN_NAME; - const dataType = (row.dataType ?? row.DATA_TYPE)?.toLowerCase(); - if (!table || !column || !dataType) return []; + const bounds = new Map(); + for (const row of rows) { + const table = row.tableName ?? row.TABLE_NAME; + const column = row.columnName ?? row.COLUMN_NAME; + if (!table || !column) continue; - if (dbType === "mysql") { - const characterLength = - row.characterMaximumLength ?? row.CHARACTER_MAXIMUM_LENGTH; - const maxIndexBytes = - characterLength === null || characterLength === undefined - ? null - : Number(characterLength) * 4; - return [ - [createDatabaseColumnKey(table, column), { maxIndexBytes }] as const, - ]; - } - - const maxLength = Number(row.maxLength ?? -1); - return [ - [ - createDatabaseColumnKey(table, column), - { maxIndexBytes: maxLength < 0 ? null : maxLength }, - ] as const, - ]; - }), - ); + const maxIndexBytes = Number( + row.maxIndexBytes ?? row.MAX_INDEX_BYTES ?? -1, + ); + bounds.set(createDatabaseColumnKey(table, column), { + maxIndexBytes: maxIndexBytes < 0 ? null : maxIndexBytes, + }); + } + return bounds; } function assertExistingTableIndexFits({ @@ -614,65 +604,71 @@ export async function getMigrations( process.exit(1); } - let currentSchema = dbType === "mssql" ? await getMssqlSchema(db) : "public"; - if (dbType === "postgres") { - currentSchema = await getPostgresSchema(db); - logger.debug( - `PostgreSQL migration: Using schema '${currentSchema}' (from search_path)`, - ); + const allTableMetadata = await db.introspection.getTables(); + let target: MigrationTarget; + let tableMetadata = allTableMetadata; + switch (dbType) { + case "postgres": { + const schema = await getPostgresSchema(db); + target = { type: "postgres", schema }; + logger.debug( + `PostgreSQL migration: Using schema '${schema}' (from search_path)`, + ); - try { - const schemas = await db.introspection.getSchemas(); - if (!schemas.some(({ name }) => name === currentSchema)) { - logger.warn( - `Schema '${currentSchema}' does not exist. Create it before running migrations or check your database configuration.`, + try { + const schemas = await db.introspection.getSchemas(); + if (!schemas.some(({ name }) => name === schema)) { + logger.warn( + `Schema '${schema}' does not exist. Create it before running migrations or check your database configuration.`, + ); + } + } catch (error) { + logger.debug( + `Could not verify schema existence: ${error instanceof Error ? error.message : String(error)}`, ); } - } catch (error) { - logger.debug( - `Could not verify schema existence: ${error instanceof Error ? error.message : String(error)}`, + + /** + * Kysely 0.28 does not expose `isForeign`, while 0.29 adds foreign table metadata. + * @see https://github.com/kysely-org/kysely/pull/1494 + */ + tableMetadata = allTableMetadata.filter( + (table) => + table.schema === schema && + !table.isView && + !("isForeign" in table && table.isForeign), ); + logger.debug( + `Found ${tableMetadata.length} table(s) in schema '${schema}': ${tableMetadata.map((table) => table.name).join(", ") || "(none)"}`, + ); + break; } - } else if (dbType === "mssql") { - logger.debug( - `SQL Server migration: Using schema '${currentSchema}' (from the current user's default schema)`, - ); + case "mssql": { + const schema = await getMssqlSchema(db); + target = { type: "mssql", schema }; + logger.debug( + `SQL Server migration: Using schema '${schema}' (from the current user's default schema)`, + ); + tableMetadata = allTableMetadata.filter( + (table) => table.schema === schema, + ); + break; + } + case "mysql": + target = { type: "mysql" }; + break; + case "sqlite": + target = { type: "sqlite" }; + break; } - const allTableMetadata = await db.introspection.getTables(); const databaseIndexMap = await getDatabaseIndexMap( db, - dbType, - currentSchema, + target, allTableMetadata.map((table) => table.name), introspectIndexes, ); - const databaseColumnBounds = await getDatabaseColumnBounds( - db, - dbType, - currentSchema, - ); - - let tableMetadata = allTableMetadata; - if (dbType === "postgres") { - /** - * Kysely 0.28 does not expose `isForeign`, while 0.29 adds foreign table metadata. - * @see https://github.com/kysely-org/kysely/pull/1494 - */ - tableMetadata = allTableMetadata.filter( - (table) => - table.schema === currentSchema && - !table.isView && - !("isForeign" in table && table.isForeign), - ); - logger.debug( - `Found ${tableMetadata.length} table(s) in schema '${currentSchema}': ${tableMetadata.map((table) => table.name).join(", ") || "(none)"}`, - ); - } else if (dbType === "mssql") { - tableMetadata = allTableMetadata.filter( - (table) => table.schema === currentSchema, - ); - } + const databaseColumnBounds = await getDatabaseColumnBounds(db, target); // Columns the migration cannot fix: required, without a default, and never // written by Better Auth. Reported so the CLI stops before an insert fails. const schemaProblems = diffSchema(