Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 53 additions & 23 deletions packages/core/src/database/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,9 @@ export function apply(db: Database) {

export function applyOnly(db: Database, input: Migration[]) {
return Effect.gen(function* () {
yield* db.run(
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
)
let completed = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
)
if (completed.size === 0) {
// Existing installs used Drizzle's migration journal. Seed the new
// journal once so TypeScript migrations don't replay old SQL.
if (
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
) {
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
SELECT name, ${Date.now()}
FROM ${sql.identifier("__drizzle_migrations")}
WHERE name IS NOT NULL
`)
completed = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
)
}
}
yield* ensureMigrationTable(db)
let completed = yield* loadCompleted(db)
if (completed.size === 0) completed = yield* importLegacyDrizzleState(db)

for (const migration of input) {
if (completed.has(migration.id)) continue
Expand All @@ -57,3 +37,53 @@ export function applyOnly(db: Database, input: Migration[]) {
}
})
}

function ensureMigrationTable(db: Database) {
return db.run(
sql`CREATE TABLE IF NOT EXISTS ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
)
}

function loadCompleted(db: Database) {
return Effect.map(
db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`),
(rows) => new Set(rows.map((row) => row.id)),
)
}

function importLegacyDrizzleState(db: Database) {
return Effect.gen(function* () {
// Existing installs used Drizzle's migration journal. Seed the new
// journal once so TypeScript migrations don't replay old SQL.
if (
!(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`))
) {
return yield* loadCompleted(db)
}

const columns = new Set(
(yield* db.all<{ name: string }>(sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`)).map(
(row) => row.name,
),
)
if (columns.has("name")) {
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
SELECT name, ${Date.now()}
FROM ${sql.identifier("__drizzle_migrations")}
WHERE name IS NOT NULL
`)
return yield* loadCompleted(db)
}

const legacyCount =
(yield* db.get<{ count: number }>(sql`SELECT count(*) as count FROM ${sql.identifier("__drizzle_migrations")}`))
?.count ?? 0
for (const id of migrations.slice(0, legacyCount).map((migration) => migration.id)) {
yield* db.run(
sql`INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${id}, ${Date.now()})`,
)
}
return yield* loadCompleted(db)
})
}
58 changes: 58 additions & 0 deletions packages/core/test/database-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,64 @@ describe("DatabaseMigration", () => {
)
})

test("imports existing drizzle migration state without a name column", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric)`,
)
yield* db.run(
sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('hash-1', 1), ('hash-2', 2)`,
)

yield* DatabaseMigration.applyOnly(db, [])

expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual(
migrations.slice(0, 2).map((migration) => ({ id: migration.id })),
)
}),
)
})

test("skips already counted legacy drizzle migrations without replaying them", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(
sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric)`,
)
yield* db.run(
sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('hash-1', 1), ('hash-2', 2)`,
)
const executed: string[] = []

yield* DatabaseMigration.applyOnly(db, [
{
id: migrations[0]!.id,
up() {
return Effect.sync(() => executed.push("first"))
},
},
{
id: migrations[1]!.id,
up() {
return Effect.sync(() => executed.push("second"))
},
},
{
id: "future",
up() {
return Effect.sync(() => executed.push("future"))
},
},
])

expect(executed).toEqual(["future"])
}),
)
})

test("does not replay a migrated session metadata column", async () => {
await run(
Effect.gen(function* () {
Expand Down
Loading