import { describe, expect, it } from "vitest"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { transform, collectKeyColumns } = require("./generate-dialect-schema.cjs") as { transform: (source: string, dialect: "postgres" | "mysql") => string; collectKeyColumns: (source: string) => { keyed: Map>; }; }; const SOURCE = `import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; import { sql } from "drizzle-orm"; export const users = sqliteTable("users", { id: text("id").primaryKey(), username: text("username").notNull(), isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false), wrapped: integer("wrapped", { mode: "boolean", }) .notNull() .default(true), score: real("score"), ssoProviderId: integer("sso_provider_id"), }); export const folders = sqliteTable("folders", { id: integer("id").primaryKey({ autoIncrement: true }), userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), name: text("name").notNull(), syncId: text("sync_id").unique(), cert: text("cert", { length: 8192 }), }); `; describe("collectKeyColumns", () => { it("finds columns that must be indexable, per table", () => { const { keyed } = collectKeyColumns(SOURCE); // primary key, unique, and both ends of the foreign key expect(keyed.get("users")?.has("id")).toBe(true); expect(keyed.get("folders")?.has("sync_id")).toBe(true); expect(keyed.get("folders")?.has("user_id")).toBe(true); }); it("leaves ordinary strings alone", () => { const { keyed } = collectKeyColumns(SOURCE); expect(keyed.get("users")?.has("username")).toBe(false); expect(keyed.get("folders")?.has("name")).toBe(false); expect(keyed.get("folders")?.has("cert")).toBe(false); }); it("does not key a column because another table indexes the same name", () => { const { keyed } = collectKeyColumns(` export const a = sqliteTable("a", { label: text("label"), }, (table) => [index("idx_a_label").on(table.label)]); export const b = sqliteTable("b", { label: text("label"), }); `); expect(keyed.get("a")?.has("label")).toBe(true); expect(keyed.get("b")?.has("label")).toBe(false); }); }); describe("postgres output", () => { const out = transform(SOURCE, "postgres"); it("is marked generated", () => { expect(out.startsWith("// GENERATED FILE")).toBe(true); }); it("uses pg-core", () => { expect(out).toContain('from "drizzle-orm/pg-core"'); expect(out).not.toContain("sqlite-core"); expect(out).toContain("pgTable("); expect(out).not.toContain("sqliteTable("); }); it("maps autoincrement keys to serial", () => { expect(out).toContain('serial("id").primaryKey()'); expect(out).not.toContain("autoIncrement"); }); it("maps integer-backed booleans, including the wrapped form", () => { expect(out).toContain('boolean("is_admin")'); // Prettier splits longer declarations across lines; both must convert. expect(out).toContain('boolean("wrapped")'); expect(out).not.toMatch(/mode:\s*"boolean"/); }); it("keeps plain integers and maps real", () => { expect(out).toContain('integer("sso_provider_id")'); expect(out).toContain('doublePrecision("score")'); }); it("makes key columns varchar and leaves the rest text", () => { expect(out).toContain('varchar("id", { length: 255 })'); expect(out).toContain('varchar("user_id", { length: 255 })'); expect(out).toContain('varchar("sync_id", { length: 255 })'); expect(out).toContain('text("username")'); expect(out).toContain('text("name")'); }); it("drops the sqlite-only text length", () => { expect(out).toContain('text("cert")'); expect(out).not.toContain("length: 8192"); }); }); describe("mysql output", () => { const out = transform(SOURCE, "mysql"); it("uses mysql-core", () => { expect(out).toContain('from "drizzle-orm/mysql-core"'); expect(out).toContain("mysqlTable("); }); it("maps autoincrement keys to int auto_increment", () => { expect(out).toContain('int("id").autoincrement().primaryKey()'); }); it("renames integer to int", () => { expect(out).toContain('int("sso_provider_id")'); expect(out).not.toMatch(/\binteger\(/); }); it("maps real to double", () => { expect(out).toContain('double("score")'); }); it("makes key columns varchar since MySQL cannot index unbounded TEXT", () => { expect(out).toContain('varchar("user_id", { length: 255 })'); expect(out).toContain('text("name")'); }); it("keeps closed scopes compact in four-column setting indexes", () => { const source = `import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"; export const pluginSettings = sqliteTable("plugin_settings", { pluginId: text("plugin_id").notNull(), scope: text("scope", { enum: ["admin", "user", "host", "secret"] }).notNull(), scopeId: text("scope_id"), key: text("key").notNull(), }, (table) => [uniqueIndex("scope_key").on(table.pluginId, table.scope, table.scopeId, table.key)]); `; const mysql = transform(source, "mysql"); expect(mysql).toContain( 'mysqlEnum("scope", ["admin", "user", "host", "secret"])', ); for (const name of ["plugin_id", "scope_id", "key"]) { expect(mysql).toContain(`varchar("${name}", { length: 255 })`); } expect(transform(source, "postgres")).toContain( 'varchar("scope", { length: 255, enum: ["admin", "user", "host", "secret"] })', ); }); }); describe("determinism", () => { it("produces identical output for identical input", () => { expect(transform(SOURCE, "postgres")).toBe(transform(SOURCE, "postgres")); expect(transform(SOURCE, "mysql")).toBe(transform(SOURCE, "mysql")); }); it("keeps foreign key behaviour verbatim", () => { for (const dialect of ["postgres", "mysql"] as const) { expect(transform(SOURCE, dialect)).toContain('onDelete: "cascade"'); } }); });