From b905bfe3d97de3c88fc31f7f2820531702df583e Mon Sep 17 00:00:00 2001 From: Taesu <166604494+bytaesu@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:49:10 +0000 Subject: [PATCH] fix(drizzle-adapter): validate schemas in relations v2 (#11213) --- .../check-drizzle-relations-v2-schema.md | 5 + .../adapter.drizzle.schema-check.test.ts | 152 ++++++++++++++++++ .../drizzle-adapter/src/relations-v2/index.ts | 29 +++- 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 .changeset/check-drizzle-relations-v2-schema.md create mode 100644 e2e/adapter/test/drizzle-v2-relations-adapter/adapter.drizzle.schema-check.test.ts diff --git a/.changeset/check-drizzle-relations-v2-schema.md b/.changeset/check-drizzle-relations-v2-schema.md new file mode 100644 index 0000000000..97cd5f02b9 --- /dev/null +++ b/.changeset/check-drizzle-relations-v2-schema.md @@ -0,0 +1,5 @@ +--- +"@better-auth/drizzle-adapter": patch +--- + +Run schema validation for the Drizzle Relations v2 adapter, including configurations that provide only relations, so schema mismatches are reported during initialization. diff --git a/e2e/adapter/test/drizzle-v2-relations-adapter/adapter.drizzle.schema-check.test.ts b/e2e/adapter/test/drizzle-v2-relations-adapter/adapter.drizzle.schema-check.test.ts new file mode 100644 index 0000000000..1e94df306b --- /dev/null +++ b/e2e/adapter/test/drizzle-v2-relations-adapter/adapter.drizzle.schema-check.test.ts @@ -0,0 +1,152 @@ +import type { BetterAuthOptions } from "@better-auth/core"; +import { + getExpectedSchema, + schemaCheckFor, +} from "@better-auth/core/db/internal"; +import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2"; +import Database from "better-sqlite3"; +import { defineRelations } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { describe, expect, it, vi } from "vitest"; + +const account = sqliteTable("account", { + id: text("id").primaryKey(), + issuer: text("issuer").notNull(), +}); + +describe("relations-v2 schema validation", () => { + it("checks relations-only configuration without querying the database", async ({ + onTestFinished, + }) => { + const client = new Database(":memory:"); + onTestFinished(() => { + client.close(); + }); + const logQuery = vi.fn(); + const db = drizzle({ + client, + relations: defineRelations({ account }), + logger: { logQuery }, + }); + const adapter = drizzleAdapter(db, { provider: "sqlite" })({}); + const check = schemaCheckFor(adapter); + expect(check).toBeTypeOf("function"); + await expect(check!()).rejects.toMatchObject({ + code: "SCHEMA_MISMATCH", + findings: expect.arrayContaining([ + { + kind: "unexpected-required-column", + table: "account", + column: "issuer", + }, + ]), + }); + expect(logQuery).not.toHaveBeenCalled(); + }); + + it("uses explicit schema overrides before relation tables", async ({ + onTestFinished, + }) => { + const client = new Database(":memory:"); + onTestFinished(() => { + client.close(); + }); + const db = drizzle({ client, relations: defineRelations({ account }) }); + const adapter = drizzleAdapter(db, { + provider: "sqlite", + schema: { + account: sqliteTable("account", { id: text("id").primaryKey() }), + }, + })({}); + const check = schemaCheckFor(adapter); + expect(check).toBeTypeOf("function"); + await expect(check!()).rejects.toMatchObject({ + findings: expect.not.arrayContaining([ + { + kind: "unexpected-required-column", + table: "account", + column: "issuer", + }, + ]), + }); + }); + + it.each([ + "relations", + "schema", + ] as const)("accepts a complete %s configuration", async (source) => { + const client = new Database(":memory:"); + try { + const expected = getExpectedSchema({}); + function tableFor(name: string) { + const columns: Record> = {}; + for (const field of Object.keys(expected[name]!.fields)) { + columns[field] = text(field); + } + return sqliteTable(name, { ...columns, id: text("id").primaryKey() }); + } + const schema = { + user: tableFor("user"), + session: tableFor("session"), + account: tableFor("account"), + verification: tableFor("verification"), + }; + const db = + source === "relations" + ? drizzle({ client, relations: defineRelations(schema) }) + : drizzle({ client, schema }); + const check = schemaCheckFor( + drizzleAdapter(db, { provider: "sqlite" })({}), + ); + expect(check).toBeTypeOf("function"); + await expect(check!()).resolves.toBeUndefined(); + } finally { + client.close(); + } + }); + + it("uses relation tables before fullSchema and preserves renamed model keys", async ({ + onTestFinished, + }) => { + const client = new Database(":memory:"); + onTestFinished(() => { + client.close(); + }); + const db = drizzle({ + client, + schema: { + identities: sqliteTable("old_account", { id: text("id").primaryKey() }), + }, + relations: defineRelations({ identities: account }), + }); + const options: BetterAuthOptions = { account: { modelName: "identities" } }; + const check = schemaCheckFor( + drizzleAdapter(db, { provider: "sqlite" })(options), + ); + expect(check).toBeTypeOf("function"); + await expect(check!()).rejects.toMatchObject({ + findings: expect.arrayContaining([ + { + kind: "unexpected-required-column", + table: "identities", + column: "issuer", + }, + ]), + }); + }); + + it("does not register a check when validation is disabled", ({ + onTestFinished, + }) => { + const client = new Database(":memory:"); + onTestFinished(() => { + client.close(); + }); + const db = drizzle({ client, relations: defineRelations({ account }) }); + const adapter = drizzleAdapter(db, { provider: "sqlite" })({ + advanced: { database: { validateSchema: false } }, + }); + expect(schemaCheckFor(adapter)).toBeUndefined(); + }); +}); diff --git a/packages/drizzle-adapter/src/relations-v2/index.ts b/packages/drizzle-adapter/src/relations-v2/index.ts index 4f2bde6a26..f279a4cc5d 100644 --- a/packages/drizzle-adapter/src/relations-v2/index.ts +++ b/packages/drizzle-adapter/src/relations-v2/index.ts @@ -7,6 +7,11 @@ import type { Where, } from "@better-auth/core/db/adapter"; import { createAdapterFactory } from "@better-auth/core/db/adapter"; +import { + checksSchema, + createSchemaCheck, + registerSchemaCheck, +} from "@better-auth/core/db/internal"; import { logger } from "@better-auth/core/env"; import { BetterAuthError } from "@better-auth/core/error"; import type { SQL } from "drizzle-orm"; @@ -39,6 +44,7 @@ import { insensitiveNe, insensitiveNotInArray, } from "../query-builders"; +import { findDrizzleSchemaProblems } from "../schema-check"; export interface DB { [key: string]: any; @@ -1128,6 +1134,27 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { const adapter = createAdapterFactory(adapterOptions); return (options: BetterAuthOptions): DBAdapter => { lazyOptions = options; - return adapter(options); + const instance = adapter(options); + if (checksSchema(options)) { + registerSchemaCheck( + instance, + createSchemaCheck(async () => { + const relations: Record = + db._?.relations ?? {}; + const schema = { + ...db._?.fullSchema, + ...Object.fromEntries( + Object.entries(relations).map(([name, relation]) => [ + name, + relation.table, + ]), + ), + ...config.schema, + }; + return findDrizzleSchemaProblems(schema, options, config.usePlural); + }, "drizzle"), + ); + } + return instance; }; };