Drizzle Schema Generator#
The Drizzle schema generator (packages/cli/src/generators/drizzle.ts) is the CLI component responsible for auto-generating Drizzle ORM table definitions from Better Auth's internal schema (BetterAuthDBSchema). It supports three database backends — pg, mysql, and sqlite — and emits fully-formatted TypeScript that can be dropped directly into a project.
Entry point#
generateDrizzleSchema is the exported SchemaGenerator function. It:
- Reads
getAuthTables(options)to obtain the canonical schema . - Resolves the target database type from
adapter.options?.providerand throws if it's absent . - Iterates every table, builds column definitions and collects indexes, then renders the final
pgTable/mysqlTable/sqliteTablecall . - Appends Drizzle
relations(...)exports for all foreign-key relationships . - Runs Prettier on the output before returning .
Column-level vs. table-level constraints#
The generator uses two independent code paths to emit uniqueness and index constraints, and their interaction is the source of a known bug.
Column-level .unique()#
At the end of each field's column definition string, .unique() is unconditionally appended whenever attr.unique is truthy :
${attr.unique ? ".unique()" : ""}
Table-level index / uniqueIndex#
Separately, while mapping over fields, the generator decides whether to push a table-level index entry into the local indexes array :
| Field flags | Emitted index type |
|---|---|
index: true, unique: false | index("…_idx") |
index: true, unique: true | uniqueIndex("…_uidx") |
index: false, unique: true (or false) | (no table-level index) |
These collected entries are later rendered as the second argument to the table constructor via assignIndexes.
Import generation#
generateImport mirrors this logic when deciding which symbols to import from drizzle-orm/${databaseType}-core. It checks field.index && !field.unique to decide whether to import index, and field.unique && field.index for uniqueIndex .
Bug: duplicate constraints when both unique and index are set#
Because the two code paths operate independently, a field with both unique: true and index: true triggers both:
- Column-level
.unique()(enforces uniqueness inline on the column). - Table-level
uniqueIndex("…_uidx")(a separate, redundant unique index).
This results in two unique constraints on the same column in the generated schema — confirmed in GitHub issue #10332. A concrete example is organization.slug from the organization plugin, which carries both flags and thus produces:
// column definition
slug: text("slug").notNull().unique()
// table-level index (redundant)
uniqueIndex("organization_slug_uidx").on(table.slug)
Proposed fix#
The minimal fix is to suppress the column-level .unique() when a table-level uniqueIndex will already be emitted :
${attr.unique && !attr.index ? ".unique()" : ""}
This defers uniqueness enforcement exclusively to the table-level uniqueIndex whenever both flags are set, eliminating the duplicate constraint.
Key files and references#
| Resource | Link |
|---|---|
| Generator source | packages/cli/src/generators/drizzle.ts |
| Bug report (issue #10332) | |
Column .unique() emission | |
| Table-level index decision | |
assignIndexes helper | |
| Import generation logic |