Unified · Split
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | import { Database } from "bun:sqlite"; |
| 2 | 2 | import { drizzle } from "drizzle-orm/bun-sqlite"; |
| 3 | 3 | import { migrate } from "drizzle-orm/bun-sqlite/migrator"; |
| 4 | −import { Context, Data, Effect, Layer, ManagedRuntime } from "effect"; | |
| 4 | +import { Context, Data, Effect, Layer, ManagedRuntime, Schema } from "effect"; | |
| 5 | 5 | import { createHash } from "node:crypto"; |
| 6 | 6 | import * as fs from "node:fs"; |
| 7 | 7 | import { homedir, tmpdir } from "node:os"; |
| @@ -165,21 +165,25 @@ export const readAppliedDrizzleMigrationHashes = (sqlite: Database): ReadonlyArr · 157 unmodified lines | ||
| 165 | 165 | .map((row) => row.hash); |
| 166 | 166 | }; |
| 167 | 167 | |
| 168 | −interface DrizzleJournal { | |
| 169 | − readonly entries: ReadonlyArray<{ | |
| 170 | − readonly idx: number; | |
| 171 | − readonly tag: string; | |
| 172 | − }>; | |
| 173 | −} | |
| 168 | +const DrizzleJournal = Schema.Struct({ | |
| 169 | + entries: Schema.Array( | |
| 170 | + Schema.Struct({ | |
| 171 | + idx: Schema.Number, | |
| 172 | + tag: Schema.String, | |
| 173 | + }), | |
| 174 | + ), | |
| 175 | +}); | |
| 176 | + | |
| 177 | +const decodeDrizzleJournal = Schema.decodeUnknownSync(Schema.fromJsonString(DrizzleJournal)); | |
| 174 | 178 | |
| 175 | 179 | export const readBundledDrizzleMigrationHashes = ( |
| 176 | 180 | migrationsFolder: string, |
| 177 | 181 | ): ReadonlyArray<string> => { |
| 178 | 182 | // Keep this in sync with drizzle-orm/src/migrator.ts: Drizzle hashes the raw |
| 179 | 183 | // migration file contents before splitting on statement breakpoints. |
| 180 | − const journal = JSON.parse( | |
| 184 | + const journal = decodeDrizzleJournal( | |
| 181 | 185 | fs.readFileSync(join(migrationsFolder, "meta", "_journal.json")).toString(), |
| 182 | − ) as DrizzleJournal; | |
| 186 | + ); | |
| 183 | 187 | |
| 184 | 188 | return [...journal.entries] |
| 185 | 189 | .sort((left, right) => left.idx - right.idx) |
| @@ -207,36 +211,37 @@ export const checkDrizzleMigrationCompatibility = (input: { · 21 unmodified lines | ||
| 207 | 211 | readonly sqlite: Database; |
| 208 | 212 | readonly dbPath: string; |
| 209 | 213 | readonly migrationsFolder: string; |
| 210 | −}): void => { | |
| 211 | − // Before running migrations, ensure the DB history is a prefix of the | |
| 212 | − // migrations bundled with this binary. This catches newer or divergent schemas | |
| 213 | − // before startup reaches arbitrary schema-dependent queries. | |
| 214 | − if (!drizzleMigrationsTableExists(input.sqlite)) return; | |
| 215 | − | |
| 216 | − const applied = readAppliedDrizzleMigrationHashes(input.sqlite); | |
| 217 | − const bundled = readBundledDrizzleMigrationHashes(input.migrationsFolder); | |
| 218 | − | |
| 219 | − if (applied.length > bundled.length) { | |
| 220 | − throw new LocalDatabaseSchemaTooNew({ | |
| 221 | − message: schemaTooNewMessage(input.dbPath), | |
| 222 | − dbPath: input.dbPath, | |
| 223 | − appliedMigrationCount: applied.length, | |
| 224 | − knownMigrationCount: bundled.length, | |
| 225 | − }); | |
| 226 | − } | |
| 227 | − | |
| 228 | − for (let index = 0; index < applied.length; index += 1) { | |
| 229 | − if (applied[index] !== bundled[index]) { | |
| 230 | − throw new LocalDatabaseMigrationHistoryMismatch({ | |
| 231 | − message: migrationHistoryMismatchMessage(input.dbPath), | |
| 214 | +}): Effect.Effect<void, LocalDatabaseSchemaTooNew | LocalDatabaseMigrationHistoryMismatch> => | |
| 215 | + Effect.gen(function* () { | |
| 216 | + // Before running migrations, ensure the DB history is a prefix of the | |
| 217 | + // migrations bundled with this binary. This catches newer or divergent schemas | |
| 218 | + // before startup reaches arbitrary schema-dependent queries. | |
| 219 | + if (!drizzleMigrationsTableExists(input.sqlite)) return; | |
| 220 | + | |
| 221 | + const applied = readAppliedDrizzleMigrationHashes(input.sqlite); | |
| 222 | + const bundled = readBundledDrizzleMigrationHashes(input.migrationsFolder); | |
| 223 | + | |
| 224 | + if (applied.length > bundled.length) { | |
| 225 | + return yield* new LocalDatabaseSchemaTooNew({ | |
| 226 | + message: schemaTooNewMessage(input.dbPath), | |
| 232 | 227 | dbPath: input.dbPath, |
| 233 | − migrationIndex: index, | |
| 234 | − appliedHash: applied[index], | |
| 235 | − knownHash: bundled[index], | |
| 228 | + appliedMigrationCount: applied.length, | |
| 229 | + knownMigrationCount: bundled.length, | |
| 236 | 230 | }); |
| 237 | 231 | } |
| 238 | − } | |
| 239 | −}; | |
| 232 | + | |
| 233 | + for (let index = 0; index < applied.length; index += 1) { | |
| 234 | + if (applied[index] !== bundled[index]) { | |
| 235 | + return yield* new LocalDatabaseMigrationHistoryMismatch({ | |
| 236 | + message: migrationHistoryMismatchMessage(input.dbPath), | |
| 237 | + dbPath: input.dbPath, | |
| 238 | + migrationIndex: index, | |
| 239 | + appliedHash: applied[index], | |
| 240 | + knownHash: bundled[index], | |
| 241 | + }); | |
| 242 | + } | |
| 243 | + } | |
| 244 | + }); | |
| 240 | 245 | |
| 241 | 246 | const createLocalExecutorLayer = () => { |
| 242 | 247 | const { path: dbPath, legacySecrets } = resolveDbPath(); |
| @@ -247,7 +252,7 @@ const createLocalExecutorLayer = () => { · 4 unmodified lines | ||
| 247 | 252 | Effect.sync(() => new Database(dbPath)), |
| 248 | 253 | (conn) => Effect.sync(() => conn.close()), |
| 249 | 254 | ); |
| 250 | − checkDrizzleMigrationCompatibility({ | |
| 255 | + yield* checkDrizzleMigrationCompatibility({ | |
| 251 | 256 | sqlite, |
| 252 | 257 | dbPath, |
| 253 | 258 | migrationsFolder: MIGRATIONS_FOLDER, |