diff --git a/packages/core/src/database/drizzle/effect-sqlite/migrator.ts b/packages/core/src/database/drizzle/effect-sqlite/migrator.ts deleted file mode 100644 index 727950fe5b8..00000000000 --- a/packages/core/src/database/drizzle/effect-sqlite/migrator.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* oxlint-disable */ -import type { MigrationConfig } from "drizzle-orm/migrator" -import { readMigrationFiles } from "drizzle-orm/migrator" -import type { AnyRelations } from "drizzle-orm/relations" -import { migrate as coreMigrate } from "../sqlite-core/effect/session.js" -import type { EffectSQLiteDatabase } from "./driver.js" - -export function migrate( - db: EffectSQLiteDatabase, - config: MigrationConfig, -) { - const migrations = readMigrationFiles(config) - return coreMigrate(migrations, db.session, config) -} diff --git a/packages/core/src/database/drizzle/index.ts b/packages/core/src/database/drizzle/index.ts index 926c601c4ef..971ec7fb997 100644 --- a/packages/core/src/database/drizzle/index.ts +++ b/packages/core/src/database/drizzle/index.ts @@ -1,6 +1,5 @@ export { EffectLogger } from "drizzle-orm/effect-core" export * from "./effect-sqlite/driver.js" export * from "./effect-sqlite/session.js" -export { migrate } from "./effect-sqlite/migrator.js" export * as EffectDrizzleSqlite from "./index.js" diff --git a/packages/core/src/database/drizzle/sqlite-core/effect/session.ts b/packages/core/src/database/drizzle/sqlite-core/effect/session.ts index 715e525ec46..a4283f4fe50 100644 --- a/packages/core/src/database/drizzle/sqlite-core/effect/session.ts +++ b/packages/core/src/database/drizzle/sqlite-core/effect/session.ts @@ -5,13 +5,10 @@ import type { SqlError } from "effect/unstable/sql/SqlError" import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect" import { NoopCache, strategyFor } from "drizzle-orm/cache/core/cache" import type { WithCacheConfig } from "drizzle-orm/cache/core/types" -import { MigratorInitError } from "drizzle-orm/effect-core/errors" import { EffectDrizzleQueryError, EffectTransactionRollbackError } from "drizzle-orm/effect-core/errors" import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger" import type { QueryEffectHKTBase, QueryEffectKind } from "drizzle-orm/effect-core/query-effect" import { entityKind, is } from "drizzle-orm/entity" -import type { MigrationConfig, MigrationMeta } from "drizzle-orm/migrator" -import { getMigrationsToRun } from "drizzle-orm/migrator.utils" import type { AnyRelations, EmptyRelations, @@ -20,17 +17,14 @@ import type { } from "drizzle-orm/relations" import { makeJitRqbMapper } from "drizzle-orm/relations" import type { PreparedQuery } from "drizzle-orm/session" -import { fillPlaceholders, type Query, type SQL, sql } from "drizzle-orm/sql/sql" +import { fillPlaceholders, type Query, type SQL } from "drizzle-orm/sql/sql" import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect" import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types" import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" -import { upgradeIfNeeded } from "../../up-migrations/effect-sqlite.js" import { assertUnreachable, makeJitQueryMapper, type RowsMapper } from "drizzle-orm/utils" import { mapResultRow, resolveNullableObjectPaths } from "../../internal/drizzle-utils.js" import { SQLiteEffectDatabase } from "./db.js" -type MigrationConfigWithInit = MigrationConfig & { init?: boolean } - type SQLiteEffectExecuteMethod = SQLiteExecuteMethod | "values" export class SQLiteEffectPreparedQuery< @@ -429,69 +423,3 @@ export abstract class SQLiteEffectTransaction< return new EffectTransactionRollbackError() } } - -export const migrate = Effect.fn("migrate")(function* ( - migrations: MigrationMeta[], - session: SQLiteEffectSession, - config: string | MigrationConfigWithInit, -) { - const migrationsTable = - typeof config === "string" ? "__drizzle_migrations" : (config.migrationsTable ?? "__drizzle_migrations") - - const { newDb } = yield* upgradeIfNeeded(migrationsTable, session, migrations) - - if (newDb) { - yield* session.run(sql` - CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} ( - id INTEGER PRIMARY KEY, - hash text NOT NULL, - created_at numeric, - name text, - applied_at TEXT - ) - `) - } - - const dbMigrations = yield* session.all<{ id: number; hash: string; created_at: string; name: string | null }>( - sql`SELECT id, hash, created_at, name FROM ${sql.identifier(migrationsTable)}`, - ) - - if (typeof config === "object" && config.init) { - if (dbMigrations.length) { - return yield* new MigratorInitError({ exitCode: "databaseMigrations" }) - } - - if (migrations.length > 1) { - return yield* new MigratorInitError({ exitCode: "localMigrations" }) - } - - const [migration] = migrations - if (!migration) return - - yield* session.run( - sql`insert into ${sql.identifier( - migrationsTable, - )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`, - ) - - return - } - - const migrationsToRun = getMigrationsToRun({ localMigrations: migrations, dbMigrations }) - if (migrationsToRun.length === 0) return - - yield* session.transaction((tx) => - Effect.gen(function* () { - for (const migration of migrationsToRun) { - for (const stmt of migration.sql) { - yield* tx.run(sql.raw(stmt)) - } - yield* tx.run( - sql`insert into ${sql.identifier( - migrationsTable, - )} ("hash", "created_at", "name", "applied_at") values(${migration.hash}, ${migration.folderMillis}, ${migration.name}, ${new Date().toISOString()})`, - ) - } - }), - ) -}) diff --git a/packages/core/src/database/drizzle/up-migrations/effect-sqlite.ts b/packages/core/src/database/drizzle/up-migrations/effect-sqlite.ts deleted file mode 100644 index d79eb8b2f51..00000000000 --- a/packages/core/src/database/drizzle/up-migrations/effect-sqlite.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* oxlint-disable */ -import * as Effect from "effect/Effect" -import type { SqlError } from "effect/unstable/sql/SqlError" -import { EffectDrizzleError } from "drizzle-orm/effect-core/errors" -import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect" -import type { MigrationMeta } from "drizzle-orm/migrator" -import { sql } from "drizzle-orm/sql/sql" -import type { SQLiteEffectSession } from "../sqlite-core/effect/session.js" -import { - buildSQLiteMigrationBackfillStatements, - prepareSQLiteMigrationBackfill, - type SQLiteMigrationTableRow, -} from "./sqlite.js" -import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils.js" - -const migrationUpgradeError = (cause: unknown) => - new EffectDrizzleError({ - message: - typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string" - ? cause.message - : String(cause), - cause, - }) - -export const upgradeIfNeeded: ( - migrationsTable: string, - session: SQLiteEffectSession, - localMigrations: MigrationMeta[], -) => Effect.Effect = - Effect.fn("upgradeIfNeeded")(function* ( - migrationsTable: string, - session: SQLiteEffectSession, - localMigrations: MigrationMeta[], - ) { - const tableExists = yield* session.all( - sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`, - ) - - if (tableExists.length === 0) { - return { newDb: true } - } - - const rows = yield* session.all<{ column_name: string }>( - sql`SELECT name as column_name FROM pragma_table_info(${migrationsTable})`, - ) - - const version = GET_VERSION_FOR.sqlite(rows.map((r) => r.column_name)) - - for (let v = version; v < MIGRATIONS_TABLE_VERSIONS.sqlite; v++) { - const upgradeFn = upgradeFunctions[v] - if (!upgradeFn) { - return yield* new EffectDrizzleError({ - message: `No upgrade path from migration table version ${v} to ${v + 1}`, - cause: { version: v }, - }) - } - yield* upgradeFn(migrationsTable, session, localMigrations) - } - - return { newDb: false } - }) - -const upgradeFunctions: Record< - number, - ( - migrationsTable: string, - session: SQLiteEffectSession, - localMigrations: MigrationMeta[], - ) => Effect.Effect -> = { - 0: upgradeFromV0, -} - -function upgradeFromV0( - migrationsTable: string, - session: SQLiteEffectSession, - localMigrations: MigrationMeta[], -): Effect.Effect { - return Effect.gen(function* () { - const table = sql`${sql.identifier(migrationsTable)}` - - const dbRows = yield* session.all( - sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`, - ) - const statements = yield* Effect.try({ - try: () => - buildSQLiteMigrationBackfillStatements( - migrationsTable, - prepareSQLiteMigrationBackfill(dbRows, localMigrations), - ), - catch: migrationUpgradeError, - }) - - yield* session.transaction((tx) => - Effect.gen(function* () { - for (const statement of statements) { - yield* tx.run(statement) - } - }), - ) - }) -} diff --git a/packages/core/src/database/drizzle/up-migrations/sqlite.ts b/packages/core/src/database/drizzle/up-migrations/sqlite.ts deleted file mode 100644 index 5356d6454de..00000000000 --- a/packages/core/src/database/drizzle/up-migrations/sqlite.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* oxlint-disable */ -import type { MigrationMeta } from "drizzle-orm/migrator" -import { type SQL, sql } from "drizzle-orm/sql/sql" - -/** @internal */ -export type SQLiteMigrationTableRow = { id: number | null; hash: string; created_at: number } - -type SQLiteMigrationBackfillEntry = { - name: string - selector: - | { column: "id"; value: number } - | { column: "created_at"; value: number } - | { column: "hash"; value: string } -} - -function unmatchedMigrationError(unmatched: SQLiteMigrationTableRow[]) { - return new Error( - `While upgrading your database migrations table we found ${unmatched.length} (${unmatched - .map((it) => `[id: ${it.id}, created_at: ${it.created_at}]`) - .join( - ", ", - )}) migrations in the database that do not match any local migration. This means that some migrations were applied to the database but are missing from the local environment`, - ) -} - -/** @internal */ -export function prepareSQLiteMigrationBackfill( - dbRows: SQLiteMigrationTableRow[], - localMigrations: MigrationMeta[], -): SQLiteMigrationBackfillEntry[] { - const sortedLocalMigrations = [...localMigrations].sort((a, b) => - a.folderMillis !== b.folderMillis ? a.folderMillis - b.folderMillis : (a.name ?? "").localeCompare(b.name ?? ""), - ) - const byMillis = new Map() - const byHash = new Map() - for (const migration of sortedLocalMigrations) { - if (!byMillis.has(migration.folderMillis)) { - byMillis.set(migration.folderMillis, []) - } - byMillis.get(migration.folderMillis)!.push(migration) - byHash.set(migration.hash, migration) - } - - const toApply: SQLiteMigrationBackfillEntry[] = [] - const unmatched: SQLiteMigrationTableRow[] = [] - - for (const dbRow of dbRows) { - const stringified = String(dbRow.created_at) - const millis = Number(stringified.substring(0, stringified.length - 3) + "000") - const candidates = byMillis.get(millis) - - const matchedByMillis = candidates?.length === 1 ? candidates[0] : undefined - const matchedByCandidateHash = - candidates && candidates.length > 1 - ? candidates.find((candidate) => candidate.hash && dbRow.hash && candidate.hash === dbRow.hash) - : undefined - const matchedByHash = matchedByMillis || matchedByCandidateHash ? undefined : byHash.get(dbRow.hash) - const matched = matchedByMillis ?? matchedByCandidateHash ?? matchedByHash - - if (matched) { - toApply.push({ - name: matched.name, - selector: - dbRow.id !== null - ? { column: "id", value: dbRow.id } - : matchedByMillis - ? { column: "created_at", value: dbRow.created_at } - : { column: "hash", value: dbRow.hash }, - }) - continue - } - - unmatched.push(dbRow) - } - - if (unmatched.length > 0) { - throw unmatchedMigrationError(unmatched) - } - - return toApply -} - -/** @internal */ -export function buildSQLiteMigrationBackfillStatements( - migrationsTable: string, - backfillEntries: SQLiteMigrationBackfillEntry[], -) { - const table = sql`${sql.identifier(migrationsTable)}` - const statements: SQL[] = [ - sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("name")} text`, - sql`ALTER TABLE ${table} ADD COLUMN ${sql.identifier("applied_at")} TEXT`, - ] - - for (const backfillEntry of backfillEntries) { - const updateQuery = sql`UPDATE ${table} SET ${sql.identifier("name")} = ${backfillEntry.name}, ${sql.identifier( - "applied_at", - )} = NULL WHERE` - - updateQuery.append(sql` ${sql.identifier(backfillEntry.selector.column)} = ${backfillEntry.selector.value}`) - - statements.push(updateQuery) - } - - return statements -} diff --git a/packages/core/src/database/drizzle/up-migrations/utils.ts b/packages/core/src/database/drizzle/up-migrations/utils.ts deleted file mode 100644 index cc71213ac40..00000000000 --- a/packages/core/src/database/drizzle/up-migrations/utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* oxlint-disable */ -export interface UpgradeResult { - newDb: boolean -} - -export const MIGRATIONS_TABLE_VERSIONS = { - sqlite: 1, - pg: 1, - effect: 1, - mysql: 1, - mssql: 1, - cockroach: 1, - singlestore: 1, -} as const - -export const GET_VERSION_FOR = { - mysql: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, - pg: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, - effect: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, - mssql: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, - cockroach: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, - singlestore: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, - sqlite: (columns: string[]): number => { - if (columns.includes("name")) return 1 - return 0 - }, -} as const diff --git a/packages/core/test/database-drizzle.test.ts b/packages/core/test/database-drizzle.test.ts index ad4f4f70051..3f0cbc6c022 100644 --- a/packages/core/test/database-drizzle.test.ts +++ b/packages/core/test/database-drizzle.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm } from "node:fs/promises" +import { mkdtemp, rm } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" import { Database } from "bun:sqlite" @@ -27,16 +27,6 @@ const makeDb = Effect.gen(function* () { return db }) -const createMigrationsFolder = async () => { - const migrationsFolder = await mkdtemp(join(tmpdir(), "effect-drizzle-sqlite-")) - await mkdir(join(migrationsFolder, "20240101000000_create_migrated_users"), { recursive: true }) - await Bun.write( - join(migrationsFolder, "20240101000000_create_migrated_users", "migration.sql"), - "create table migrated_users (id integer primary key autoincrement, name text not null);", - ) - return migrationsFolder -} - test("selects rows through Effect-yieldable query builders", async () => { await run( Effect.gen(function* () { @@ -173,25 +163,3 @@ test("supports returning and rejects empty update sets", async () => { }), ) }) - -test("runs migrations once and records migration metadata", async () => { - const migrationsFolder = await createMigrationsFolder() - try { - await run( - Effect.gen(function* () { - const db = yield* EffectDrizzleSqlite.makeWithDefaults() - - yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder }) - yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder }) - yield* db.run(sql`insert into migrated_users (name) values ('Margaret')`) - - expect(yield* db.all<{ name: string }>(sql`select name from migrated_users`)).toEqual([{ name: "Margaret" }]) - expect(yield* db.all<{ name: string | null }>(sql`select name from __drizzle_migrations`)).toEqual([ - { name: "20240101000000_create_migrated_users" }, - ]) - }), - ) - } finally { - await rm(migrationsFolder, { recursive: true, force: true }) - } -})