@@ -595,14 +595,16 @@ IndexedDB-style event-based migrations:
595595db .addEventListener (" upgradeneeded" , (e ) => {
596596 e .waitUntil ((async () => {
597597 if (e .oldVersion < 1 ) {
598- await db .exec ` ${ Users . ddl ()} ` ;
599- await db .exec ` ${ Posts . ddl ()} ` ;
598+ await db .ensureTable ( Users ) ;
599+ await db .ensureTable ( Posts ) ;
600600 }
601601 if (e .oldVersion < 2 ) {
602- await db .exec ` ${Posts .ensureColumn (" views" )} ` ;
602+ // Add a new column - just update the schema and call ensureTable again
603+ await db .ensureTable (Posts ); // Adds missing "views" column
603604 }
604605 if (e .oldVersion < 3 ) {
605- await db .exec ` ${Posts .ensureIndex ([" title" ])} ` ;
606+ // Add constraints after data cleanup
607+ await db .ensureConstraints (Posts );
606608 }
607609 })());
608610});
@@ -623,42 +625,51 @@ await db.open(3); // Opens at version 3, fires upgradeneeded if needed
623625zen provides idempotent helpers that encourage safe, additive-only migrations:
624626
625627``` typescript
626- // Add a new column (reads from schema)
628+ // Add a new column - update schema and call ensureTable
627629const Posts = table (" posts" , {
628630 id: z .string ().db .primary (),
629631 title: z .string (),
630632 views: z .number ().db .inserted (() => 0 ), // NEW - add to schema
631633});
632634
633635if (e .oldVersion < 2 ) {
634- await db .exec ` ${ Posts . ensureColumn ( " views " )} ` ;
636+ await db .ensureTable ( Posts ); // Adds missing columns from schema
635637}
636- // → ALTER TABLE "posts" ADD COLUMN IF NOT EXISTS "views" REAL DEFAULT 0
638+ // → ALTER TABLE "posts" ADD COLUMN "views" REAL DEFAULT 0
639+
640+ // Add indexes - defined in schema, applied by ensureTable
641+ const Posts = table (" posts" , {
642+ id: z .string ().db .primary (),
643+ title: z .string ().db .index (), // NEW - add index
644+ views: z .number ().db .inserted (() => 0 ),
645+ });
637646
638- // Add an index
639647if (e .oldVersion < 3 ) {
640- await db .exec ` ${ Posts . ensureIndex ([ " title " , " views " ])} ` ;
648+ await db .ensureTable ( Posts ); // Adds missing indexes
641649}
642- // → CREATE INDEX IF NOT EXISTS "idx_posts_title_views " ON "posts"("title", "views ")
650+ // → CREATE INDEX IF NOT EXISTS "idx_posts_title " ON "posts"("title")
643651
644652// Safe column rename (additive, non-destructive)
645653const Users = table (" users" , {
646- emailAddress: z .string ().email (), // renamed from "email"
654+ id: z .string ().db .primary (),
655+ email: z .string ().email (), // Keep old column
656+ emailAddress: z .string ().email (), // NEW - add new column
647657});
648658
649659if (e .oldVersion < 4 ) {
650- await db .exec ` ${ Users . ensureColumn ( " emailAddress " )} ` ;
651- await db .exec ` ${ Users . copyColumn (" email" , " emailAddress" )} ` ;
660+ await db .ensureTable ( Users ); // Adds emailAddress column
661+ await db .copyColumn (Users , " email" , " emailAddress" ); // Copy data
652662 // Keep old "email" column for backwards compat
653663 // Drop it in a later migration if needed (manual SQL)
654664}
655665// → UPDATE "users" SET "emailAddress" = "email" WHERE "emailAddress" IS NULL
656666```
657667
658668** Helper methods:**
659- - ` table.ensureColumn(fieldName, options?) ` - Idempotent ALTER TABLE ADD COLUMN
660- - ` table.ensureIndex(fields, options?) ` - Idempotent CREATE INDEX
661- - ` table.copyColumn(from, to) ` - Copy data between columns (for safe renames)
669+ - ` db.ensureTable(table) ` - Idempotent CREATE TABLE / ADD COLUMN / CREATE INDEX
670+ - ` db.ensureView(view) ` - Idempotent DROP + CREATE VIEW
671+ - ` db.ensureConstraints(table) ` - Add unique/FK constraints (with preflight checks)
672+ - ` db.copyColumn(table, from, to) ` - Copy data between columns (for safe renames)
662673
663674All helpers read from your table schema (single source of truth) and are safe to run multiple times (idempotent).
664675
@@ -937,25 +948,9 @@ const query = db.print`SELECT * FROM ${Posts} WHERE ${Posts.cols.published} = ${
937948console .log (query .sql ); // SELECT * FROM "posts" WHERE "posts"."published" = ?
938949console .log (query .params ); // [true]
939950
940- // Inspect DDL generation
941- const ddl = db .print ` ${Posts .ddl ()} ` ;
942- console .log (ddl .sql ); // CREATE TABLE IF NOT EXISTS "posts" (...)
943-
944- // Analyze query execution plan
945- const plan = await db .explain `
946- SELECT * FROM ${Posts }
947- WHERE ${Posts .cols .authorId } = ${userId }
948- ` ;
949- console .log (plan );
950- // SQLite: [{ detail: "SEARCH posts USING INDEX idx_posts_authorId (authorId=?)" }]
951- // PostgreSQL: [{ "QUERY PLAN": "Index Scan using idx_posts_authorId on posts" }]
952-
953951// Debug fragments
954952console .log (Posts .set ({ title: " Updated" }).toString ());
955953// SQLFragment { sql: "\"title\" = ?", params: ["Updated"] }
956-
957- console .log (Posts .ddl ().toString ());
958- // DDLFragment { type: "create-table", table: "posts" }
959954```
960955
961956## Dialect Support
@@ -1085,12 +1080,6 @@ const Posts = table("posts", {
10851080
10861081const rows = [{id: " u1" , email: " alice@example.com" , deletedAt: null }];
10871082
1088- // DDL Generation
1089- Users .ddl (); // DDLFragment for CREATE TABLE
1090- Users .ensureColumn (" emailAddress" ); // DDLFragment for ALTER TABLE ADD COLUMN
1091- Users .ensureIndex ([" email" ]); // DDLFragment for CREATE INDEX
1092- Users .copyColumn (" email" , " emailAddress" ); // SQLFragment for UPDATE (copy data)
1093-
10941083// Query Fragments
10951084Users .set ({email: " alice@example.com" }); // SQLFragment for SET clause
10961085Users .values (rows ); // SQLFragment for INSERT VALUES
@@ -1157,9 +1146,15 @@ await db.transaction(async (tx) => {
11571146 await tx .exec ` SELECT 1 ` ;
11581147});
11591148
1149+ // Schema Management
1150+ await db .ensureTable (Users ); // CREATE TABLE / ADD COLUMN / CREATE INDEX
1151+ await db .ensureView (AdminUsers ); // DROP + CREATE VIEW
1152+ await db .ensureConstraints (Users ); // Add unique/FK constraints
1153+ await db .copyColumn (Users , " old" , " new" ); // Copy data between columns
1154+
11601155// Debugging
1161- db .print ` SELECT 1 ` ;
1162- await db .explain ` SELECT * FROM ${Users } ` ;
1156+ db .print ` SELECT 1 ` ; // Returns { sql, params } without executing
1157+ await db .explain ` SELECT * FROM ${Users } ` ; // Returns query execution plan
11631158```
11641159
11651160### Driver Exports
0 commit comments