Unified · Split
| @@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs"; · 2 unmodified lines | ||
| 3 | 3 | import { tmpdir } from "node:os"; |
| 4 | 4 | import { join } from "node:path"; |
| 5 | 5 | import { afterEach, describe, expect, it } from "@effect/vitest"; |
| 6 | +import { Effect } from "effect"; | |
| 6 | 7 | import { drizzle } from "drizzle-orm/bun-sqlite"; |
| 7 | 8 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; |
| 8 | 9 | |
| @@ -17,12 +18,15 @@ import { · 8 unmodified lines | ||
| 17 | 18 | const MIGRATIONS_FOLDER = join(import.meta.dirname, "../../drizzle"); |
| 18 | 19 | |
| 19 | 20 | const workDirs: string[] = []; |
| 21 | +const openDbs: Database[] = []; | |
| 20 | 22 | |
| 21 | 23 | const tempDb = (): { db: Database; path: string } => { |
| 22 | 24 | const dir = mkdtempSync(join(tmpdir(), "executor-schema-compat-")); |
| 23 | 25 | workDirs.push(dir); |
| 24 | 26 | const path = join(dir, "data.db"); |
| 25 | − return { db: new Database(path), path }; | |
| 27 | + const db = new Database(path); | |
| 28 | + openDbs.push(db); | |
| 29 | + return { db, path }; | |
| 26 | 30 | }; |
| 27 | 31 | |
| 28 | 32 | const createMigrationTable = (db: Database): void => { |
| @@ -43,139 +47,105 @@ const insertMigrationHashes = (db: Database, hashes: ReadonlyArray<string>): voi · 14 unmodified lines | ||
| 43 | 47 | }; |
| 44 | 48 | |
| 45 | 49 | afterEach(() => { |
| 50 | + for (const db of openDbs.splice(0)) { | |
| 51 | + db.close(); | |
| 52 | + } | |
| 46 | 53 | for (const dir of workDirs.splice(0)) { |
| 47 | 54 | rmSync(dir, { recursive: true, force: true }); |
| 48 | 55 | } |
| 49 | 56 | }); |
| 50 | 57 | |
| 51 | 58 | describe("Drizzle migration compatibility preflight", () => { |
| 52 | − it("allows a fresh DB without __drizzle_migrations", () => { | |
| 53 | − const { db, path } = tempDb(); | |
| 54 | − try { | |
| 55 | − expect(() => | |
| 56 | − checkDrizzleMigrationCompatibility({ | |
| 57 | − sqlite: db, | |
| 58 | − dbPath: path, | |
| 59 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 60 | − }), | |
| 61 | − ).not.toThrow(); | |
| 62 | − } finally { | |
| 63 | − db.close(); | |
| 64 | − } | |
| 65 | − }); | |
| 66 | − | |
| 67 | − it("allows an existing but empty __drizzle_migrations table", () => { | |
| 68 | − const { db, path } = tempDb(); | |
| 69 | − try { | |
| 59 | + it.effect("allows a fresh DB without __drizzle_migrations", () => | |
| 60 | + Effect.gen(function* () { | |
| 61 | + const { db, path } = tempDb(); | |
| 62 | + | |
| 63 | + yield* checkDrizzleMigrationCompatibility({ | |
| 64 | + sqlite: db, | |
| 65 | + dbPath: path, | |
| 66 | + migrationsFolder: MIGRATIONS_FOLDER, | |
| 67 | + }); | |
| 68 | + }), | |
| 69 | + ); | |
| 70 | + | |
| 71 | + it.effect("allows an existing but empty __drizzle_migrations table", () => | |
| 72 | + Effect.gen(function* () { | |
| 73 | + const { db, path } = tempDb(); | |
| 70 | 74 | createMigrationTable(db); |
| 71 | 75 | |
| 72 | − expect(() => | |
| 73 | − checkDrizzleMigrationCompatibility({ | |
| 74 | − sqlite: db, | |
| 75 | − dbPath: path, | |
| 76 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 77 | − }), | |
| 78 | − ).not.toThrow(); | |
| 79 | − } finally { | |
| 80 | − db.close(); | |
| 81 | − } | |
| 82 | − }); | |
| 76 | + yield* checkDrizzleMigrationCompatibility({ | |
| 77 | + sqlite: db, | |
| 78 | + dbPath: path, | |
| 79 | + migrationsFolder: MIGRATIONS_FOLDER, | |
| 80 | + }); | |
| 81 | + }), | |
| 82 | + ); | |
| 83 | 83 | |
| 84 | 84 | it("computes bundled hashes that exactly match hashes written by Drizzle", () => { |
| 85 | 85 | const { db } = tempDb(); |
| 86 | − try { | |
| 87 | − migrate(drizzle(db), { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 88 | − | |
| 89 | − expect(readAppliedDrizzleMigrationHashes(db)).toEqual( | |
| 90 | − readBundledDrizzleMigrationHashes(MIGRATIONS_FOLDER), | |
| 91 | − ); | |
| 92 | − } finally { | |
| 93 | − db.close(); | |
| 94 | − } | |
| 86 | + migrate(drizzle(db), { migrationsFolder: MIGRATIONS_FOLDER }); | |
| 87 | + | |
| 88 | + expect(readAppliedDrizzleMigrationHashes(db)).toEqual( | |
| 89 | + readBundledDrizzleMigrationHashes(MIGRATIONS_FOLDER), | |
| 90 | + ); | |
| 95 | 91 | }); |
| 96 | 92 | |
| 97 | − it("throws LocalDatabaseSchemaTooNew when the DB has more migrations than the binary", () => { | |
| 98 | − const { db, path } = tempDb(); | |
| 99 | − try { | |
| 93 | + it.effect("fails with LocalDatabaseSchemaTooNew when the DB has more migrations", () => | |
| 94 | + Effect.gen(function* () { | |
| 95 | + const { db, path } = tempDb(); | |
| 100 | 96 | const bundled = readBundledDrizzleMigrationHashes(MIGRATIONS_FOLDER); |
| 101 | 97 | createMigrationTable(db); |
| 102 | 98 | insertMigrationHashes(db, [...bundled, "future-migration-hash"]); |
| 103 | 99 | |
| 104 | − expect(() => | |
| 105 | − checkDrizzleMigrationCompatibility({ | |
| 106 | − sqlite: db, | |
| 107 | − dbPath: path, | |
| 108 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 109 | − }), | |
| 110 | − ).toThrow(LocalDatabaseSchemaTooNew); | |
| 111 | − | |
| 112 | − try { | |
| 113 | − checkDrizzleMigrationCompatibility({ | |
| 114 | − sqlite: db, | |
| 115 | − dbPath: path, | |
| 116 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 117 | − }); | |
| 118 | − } catch (error) { | |
| 119 | − expect(error).toBeInstanceOf(LocalDatabaseSchemaTooNew); | |
| 120 | − expect((error as Error).message).toContain( | |
| 121 | − "This Executor binary is older than the schema", | |
| 122 | − ); | |
| 123 | − expect((error as Error).message).toContain("Use a newer Executor binary"); | |
| 124 | − } | |
| 125 | − } finally { | |
| 126 | − db.close(); | |
| 127 | − } | |
| 128 | − }); | |
| 129 | − | |
| 130 | − it("throws LocalDatabaseMigrationHistoryMismatch when hashes diverge", () => { | |
| 131 | − const { db, path } = tempDb(); | |
| 132 | − try { | |
| 100 | + const error = yield* checkDrizzleMigrationCompatibility({ | |
| 101 | + sqlite: db, | |
| 102 | + dbPath: path, | |
| 103 | + migrationsFolder: MIGRATIONS_FOLDER, | |
| 104 | + }).pipe(Effect.flip); | |
| 105 | + | |
| 106 | + expect(error).toBeInstanceOf(LocalDatabaseSchemaTooNew); | |
| 107 | + expect(error).toMatchObject({ | |
| 108 | + message: expect.stringContaining("This Executor binary is older than the schema"), | |
| 109 | + }); | |
| 110 | + expect(error).toMatchObject({ | |
| 111 | + message: expect.stringContaining("Use a newer Executor binary"), | |
| 112 | + }); | |
| 113 | + }), | |
| 114 | + ); | |
| 115 | + | |
| 116 | + it.effect("fails with LocalDatabaseMigrationHistoryMismatch when hashes diverge", () => | |
| 117 | + Effect.gen(function* () { | |
| 118 | + const { db, path } = tempDb(); | |
| 133 | 119 | const bundled = readBundledDrizzleMigrationHashes(MIGRATIONS_FOLDER); |
| 134 | 120 | createMigrationTable(db); |
| 135 | 121 | insertMigrationHashes(db, ["different-migration-hash", ...bundled.slice(1)]); |
| 136 | 122 | |
| 137 | − expect(() => | |
| 138 | − checkDrizzleMigrationCompatibility({ | |
| 139 | − sqlite: db, | |
| 140 | − dbPath: path, | |
| 141 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 142 | − }), | |
| 143 | − ).toThrow(LocalDatabaseMigrationHistoryMismatch); | |
| 144 | − | |
| 145 | − try { | |
| 146 | − checkDrizzleMigrationCompatibility({ | |
| 147 | − sqlite: db, | |
| 148 | − dbPath: path, | |
| 149 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 150 | − }); | |
| 151 | − } catch (error) { | |
| 152 | − expect(error).toBeInstanceOf(LocalDatabaseMigrationHistoryMismatch); | |
| 153 | − expect((error as Error).message).toContain( | |
| 154 | − "does not match this Executor build", | |
| 155 | − ); | |
| 156 | − expect((error as Error).message).toContain("restore a backup"); | |
| 157 | − } | |
| 158 | − } finally { | |
| 159 | − db.close(); | |
| 160 | − } | |
| 161 | − }); | |
| 162 | − | |
| 163 | − it("allows an older DB whose migration history is a bundled prefix", () => { | |
| 164 | − const { db, path } = tempDb(); | |
| 165 | − try { | |
| 123 | + const error = yield* checkDrizzleMigrationCompatibility({ | |
| 124 | + sqlite: db, | |
| 125 | + dbPath: path, | |
| 126 | + migrationsFolder: MIGRATIONS_FOLDER, | |
| 127 | + }).pipe(Effect.flip); | |
| 128 | + | |
| 129 | + expect(error).toBeInstanceOf(LocalDatabaseMigrationHistoryMismatch); | |
| 130 | + expect(error).toMatchObject({ | |
| 131 | + message: expect.stringContaining("does not match this Executor build"), | |
| 132 | + }); | |
| 133 | + expect(error).toMatchObject({ message: expect.stringContaining("restore a backup") }); | |
| 134 | + }), | |
| 135 | + ); | |
| 136 | + | |
| 137 | + it.effect("allows an older DB whose migration history is a bundled prefix", () => | |
| 138 | + Effect.gen(function* () { | |
| 139 | + const { db, path } = tempDb(); | |
| 166 | 140 | const bundled = readBundledDrizzleMigrationHashes(MIGRATIONS_FOLDER); |
| 167 | 141 | createMigrationTable(db); |
| 168 | 142 | insertMigrationHashes(db, bundled.slice(0, 1)); |
| 169 | 143 | |
| 170 | − expect(() => | |
| 171 | − checkDrizzleMigrationCompatibility({ | |
| 172 | − sqlite: db, | |
| 173 | − dbPath: path, | |
| 174 | − migrationsFolder: MIGRATIONS_FOLDER, | |
| 175 | − }), | |
| 176 | − ).not.toThrow(); | |
| 177 | − } finally { | |
| 178 | − db.close(); | |
| 179 | − } | |
| 180 | − }); | |
| 144 | + yield* checkDrizzleMigrationCompatibility({ | |
| 145 | + sqlite: db, | |
| 146 | + dbPath: path, | |
| 147 | + migrationsFolder: MIGRATIONS_FOLDER, | |
| 148 | + }); | |
| 149 | + }), | |
| 150 | + ); | |
| 181 | 151 | }); |