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