Pārlūkot izejas kodu

feat(effect-drizzle-sqlite): add vendored sqlite adapter (#28547)

Kit Langton 2 mēneši atpakaļ
vecāks
revīzija
5381795844
28 mainītis faili ar 3697 papildinājumiem un 0 dzēšanām
  1. 19 0
      bun.lock
  2. 1 0
      package.json
  3. 19 0
      packages/effect-drizzle-sqlite/AGENTS.md
  4. 92 0
      packages/effect-drizzle-sqlite/examples/basic.ts
  5. 4 0
      packages/effect-drizzle-sqlite/examples/migrations/20240101000000_create_users/migration.sql
  6. 29 0
      packages/effect-drizzle-sqlite/package.json
  7. 77 0
      packages/effect-drizzle-sqlite/src/effect-sqlite/driver.ts
  8. 4 0
      packages/effect-drizzle-sqlite/src/effect-sqlite/index.ts
  9. 14 0
      packages/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts
  10. 214 0
      packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts
  11. 6 0
      packages/effect-drizzle-sqlite/src/index.ts
  12. 127 0
      packages/effect-drizzle-sqlite/src/internal/drizzle-utils.ts
  13. 58 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts
  14. 296 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts
  15. 261 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts
  16. 10 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/index.ts
  17. 349 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts
  18. 198 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts
  19. 49 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts
  20. 279 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts
  21. 490 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts
  22. 402 0
      packages/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts
  23. 102 0
      packages/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts
  24. 253 0
      packages/effect-drizzle-sqlite/src/up-migrations/sqlite.ts
  25. 45 0
      packages/effect-drizzle-sqlite/src/up-migrations/utils.ts
  26. 139 0
      packages/effect-drizzle-sqlite/test/sqlite.test.ts
  27. 15 0
      packages/effect-drizzle-sqlite/tsconfig.json
  28. 145 0
      specs/storage/effect-sqlite-package.md

+ 19 - 0
bun.lock

@@ -305,6 +305,20 @@
         "@parcel/watcher-win32-x64": "2.5.1",
       },
     },
+    "packages/effect-drizzle-sqlite": {
+      "name": "@opencode-ai/effect-drizzle-sqlite",
+      "version": "1.15.5",
+      "dependencies": {
+        "drizzle-orm": "catalog:",
+        "effect": "catalog:",
+      },
+      "devDependencies": {
+        "@effect/sql-sqlite-bun": "catalog:",
+        "@tsconfig/bun": "catalog:",
+        "@types/bun": "catalog:",
+        "@typescript/native-preview": "catalog:",
+      },
+    },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
       "version": "1.15.6",
@@ -717,6 +731,7 @@
     "@cloudflare/workers-types": "4.20251008.0",
     "@effect/opentelemetry": "4.0.0-beta.66",
     "@effect/platform-node": "4.0.0-beta.66",
+    "@effect/sql-sqlite-bun": "4.0.0-beta.66",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@lydell/node-pty": "1.2.0-beta.10",
@@ -1114,6 +1129,8 @@
 
     "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.66", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-+ymrhBnESv/hmn5SKTe2//IY9Ox/hGPeoogEWhW47ZGyhFI5eMYFxdEUBa+3IAV05rrBzrxON9lynu68n0DM7w=="],
 
+    "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.66", "", { "peerDependencies": { "effect": "^4.0.0-beta.66" } }, "sha512-UYsrAb/5T0ZRypeN9Kmv3/ZInibGCjM6dtoiAWtfG+xKyuq8N05wmuVCXB0+XgVmUBxDWjw/S1fu4ivS0vZVuw=="],
+
     "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
 
     "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
@@ -1542,6 +1559,8 @@
 
     "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"],
 
+    "@opencode-ai/effect-drizzle-sqlite": ["@opencode-ai/effect-drizzle-sqlite@workspace:packages/effect-drizzle-sqlite"],
+
     "@opencode-ai/enterprise": ["@opencode-ai/enterprise@workspace:packages/enterprise"],
 
     "@opencode-ai/function": ["@opencode-ai/function@workspace:packages/function"],

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "catalog": {
       "@effect/opentelemetry": "4.0.0-beta.66",
       "@effect/platform-node": "4.0.0-beta.66",
+      "@effect/sql-sqlite-bun": "4.0.0-beta.66",
       "@npmcli/arborist": "9.4.0",
       "@types/bun": "1.3.13",
       "@types/cross-spawn": "6.0.6",

+ 19 - 0
packages/effect-drizzle-sqlite/AGENTS.md

@@ -0,0 +1,19 @@
+# Effect Drizzle SQLite
+
+This package vendors a Drizzle Effect SQLite adapter for this repo.
+
+- Keep this package generic: Drizzle + Effect + SQLite only.
+- Do not add opencode-specific tables, paths, migrations, post-commit hooks, or domain storage APIs here.
+- Runtime code should depend on generic `effect/unstable/sql/SqlClient`, not a specific SQLite driver.
+- Concrete SQLite clients such as `@effect/sql-sqlite-bun` belong in tests or examples unless this package intentionally adds a driver-specific helper.
+- Preserve Drizzle adapter naming and behavior where possible so this can be replaced by upstream `drizzle-orm/effect-sqlite` later.
+- If touching copied Drizzle internals, compare with current `drizzle-orm@1.0.0-rc.2` declarations and runtime JS.
+- If touching Effect APIs, verify against `/Users/kit/code/open-source/effect-smol`.
+
+Useful entry points:
+
+- `src/effect-sqlite/driver.ts`: creates the Effect-backed Drizzle database with `make` and `makeWithDefaults`.
+- `src/effect-sqlite/session.ts`: adapts generic Effect `SqlClient` execution and transactions to Drizzle SQLite sessions.
+- `src/sqlite-core/effect/*`: Effect-yieldable SQLite query builders.
+- `src/internal/drizzle-utils.ts`: local typed shims for Drizzle runtime internals that RC2 does not expose in declarations.
+- `examples/basic.ts`: minimal usage example with Bun SQLite.

+ 92 - 0
packages/effect-drizzle-sqlite/examples/basic.ts

@@ -0,0 +1,92 @@
+import { SqliteClient } from "@effect/sql-sqlite-bun"
+import { eq } from "drizzle-orm"
+import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
+import * as Context from "effect/Context"
+import * as Effect from "effect/Effect"
+import * as Layer from "effect/Layer"
+import * as Schema from "effect/Schema"
+import { EffectDrizzleSqlite } from "../src"
+
+const users = sqliteTable("users", {
+  id: integer().primaryKey({ autoIncrement: true }),
+  name: text().notNull(),
+})
+
+type User = typeof users.$inferSelect
+
+const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
+type DatabaseShape = Effect.Success<typeof makeDatabase>
+
+const sqliteLayer = SqliteClient.layer({ filename: ":memory:", disableWAL: true })
+
+class Database extends Context.Service<Database, DatabaseShape>()("@opencode/example/Database") {
+  static layer = Layer.effect(Database, makeDatabase).pipe(Layer.provide(sqliteLayer))
+}
+
+class UserStoreError extends Schema.TaggedErrorClass<UserStoreError>()("UserStoreError", {
+  message: Schema.String,
+  cause: Schema.optional(Schema.Defect),
+}) {}
+
+const mapStoreError = (message: string) => (cause: unknown) => new UserStoreError({ message, cause })
+
+interface UserStoreShape {
+  migrate(): Effect.Effect<void, UserStoreError>
+  create(name: string): Effect.Effect<void, UserStoreError>
+  rename(from: string, to: string): Effect.Effect<void, UserStoreError>
+  list(): Effect.Effect<User[], UserStoreError>
+}
+
+class UserStore extends Context.Service<UserStore, UserStoreShape>()("@opencode/example/UserStore") {
+  static layer = Layer.effect(
+    UserStore,
+    Effect.gen(function* () {
+      const db = yield* Database
+
+      return UserStore.of({
+        migrate: Effect.fn("UserStore.migrate")(function* () {
+          yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: `${import.meta.dirname}/migrations` }).pipe(
+            Effect.mapError((cause) => new UserStoreError({ message: "Failed to migrate users", cause })),
+          )
+        }),
+        create: Effect.fn("UserStore.create")(function* (name: string) {
+          yield* db
+            .insert(users)
+            .values({ name })
+            .pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to create user")))
+        }),
+        rename: Effect.fn("UserStore.rename")(function* (from: string, to: string) {
+          yield* db
+            .transaction(
+              Effect.fnUntraced(function* (tx) {
+                yield* tx.insert(users).values({ name: from })
+                yield* tx.update(users).set({ name: to }).where(eq(users.name, from))
+              }),
+              { behavior: "immediate" },
+            )
+            .pipe(Effect.asVoid, Effect.mapError(mapStoreError("Failed to rename user")))
+        }),
+        list: Effect.fn("UserStore.list")(function* () {
+          return yield* db
+            .select()
+            .from(users)
+            .pipe(Effect.mapError(mapStoreError("Failed to list users")))
+        }),
+      })
+    }),
+  ).pipe(Layer.provide(Database.layer))
+}
+
+const program = Effect.gen(function* () {
+  const userStore = yield* UserStore
+
+  yield* userStore.migrate()
+  yield* userStore.create("Ada")
+  yield* userStore.rename("Grace", "Grace Hopper")
+
+  return yield* userStore.list()
+})
+
+const rows = await Effect.runPromise(program.pipe(Effect.provide(UserStore.layer)))
+
+console.log(rows)

+ 4 - 0
packages/effect-drizzle-sqlite/examples/migrations/20240101000000_create_users/migration.sql

@@ -0,0 +1,4 @@
+CREATE TABLE users (
+  id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+  name text NOT NULL
+);

+ 29 - 0
packages/effect-drizzle-sqlite/package.json

@@ -0,0 +1,29 @@
+{
+  "$schema": "https://json.schemastore.org/package.json",
+  "version": "1.15.5",
+  "name": "@opencode-ai/effect-drizzle-sqlite",
+  "type": "module",
+  "license": "MIT",
+  "private": true,
+  "scripts": {
+    "test": "bun test --timeout 30000",
+    "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
+    "typecheck": "tsgo --noEmit"
+  },
+  "exports": {
+    ".": "./src/index.ts",
+    "./effect-sqlite": "./src/effect-sqlite/index.ts",
+    "./effect-sqlite/migrator": "./src/effect-sqlite/migrator.ts",
+    "./sqlite-core/effect": "./src/sqlite-core/effect/index.ts"
+  },
+  "devDependencies": {
+    "@effect/sql-sqlite-bun": "catalog:",
+    "@tsconfig/bun": "catalog:",
+    "@types/bun": "catalog:",
+    "@typescript/native-preview": "catalog:"
+  },
+  "dependencies": {
+    "drizzle-orm": "catalog:",
+    "effect": "catalog:"
+  }
+}

+ 77 - 0
packages/effect-drizzle-sqlite/src/effect-sqlite/driver.ts

@@ -0,0 +1,77 @@
+/* oxlint-disable */
+import * as Effect from "effect/Effect"
+import * as Layer from "effect/Layer"
+import { SqlClient } from "effect/unstable/sql/SqlClient"
+import { EffectCache } from "drizzle-orm/cache/core/cache-effect"
+import { EffectLogger } from "drizzle-orm/effect-core"
+import { entityKind } from "drizzle-orm/entity"
+import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
+import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+import { SQLiteEffectDatabase } from "../sqlite-core/effect/db"
+import type { DrizzleConfig } from "drizzle-orm/utils"
+import { jitCompatCheck } from "../internal/drizzle-utils"
+import { type EffectSQLiteQueryEffectHKT, type EffectSQLiteRunResult, EffectSQLiteSession } from "./session"
+
+export class EffectSQLiteDatabase<TRelations extends AnyRelations = EmptyRelations> extends SQLiteEffectDatabase<
+  EffectSQLiteQueryEffectHKT,
+  EffectSQLiteRunResult,
+  TRelations
+> {
+  static override readonly [entityKind]: string = "EffectSQLiteDatabase"
+}
+
+export type EffectDrizzleSQLiteConfig<TRelations extends AnyRelations = EmptyRelations> = Omit<
+  DrizzleConfig<Record<string, never>, TRelations>,
+  "cache" | "logger" | "schema"
+>
+
+export const DefaultServices = Layer.merge(EffectCache.Default, EffectLogger.Default)
+
+/**
+ * Creates an EffectSQLiteDatabase instance.
+ *
+ * Requires a generic Effect `SqlClient`, `EffectLogger`, and `EffectCache` services to be provided.
+ * Drizzle only depends on the generic `SqlClient`; install and provide a compatible SQLite provider such as
+ * `@effect/sql-sqlite-node`, `@effect/sql-sqlite-bun`, or another package that exposes `SqlClient`.
+ *
+ * @example
+ * ```ts
+ * import { SqliteClient } from '@effect/sql-sqlite-node';
+ * import * as SQLiteDrizzle from 'drizzle-orm/effect-sqlite';
+ * import * as Effect from 'effect/Effect';
+ *
+ * const db = yield* SQLiteDrizzle.make({ relations }).pipe(
+ *   Effect.provide(SQLiteDrizzle.DefaultServices),
+ *   Effect.provide(SqliteClient.layer({ filename: 'sqlite.db' })),
+ * );
+ * ```
+ */
+export const make = Effect.fn("SQLiteDrizzle.make")(function* <TRelations extends AnyRelations = EmptyRelations>(
+  config: EffectDrizzleSQLiteConfig<TRelations> = {},
+) {
+  const client = yield* SqlClient
+  const cache = yield* EffectCache
+  const logger = yield* EffectLogger
+
+  const dialect = new SQLiteAsyncDialect()
+  const relations = config.relations ?? ({} as TRelations)
+  const session = new EffectSQLiteSession(client, dialect, relations, {
+    logger,
+    cache,
+    useJitMappers: jitCompatCheck(config.jit),
+  })
+  const db = new EffectSQLiteDatabase(dialect, session, relations) as EffectSQLiteDatabase<TRelations> & {
+    $client: SqlClient
+  }
+  db.$client = client
+  db.$cache.invalidate = cache.onMutate
+
+  return db
+})
+
+/**
+ * Convenience function that creates an EffectSQLiteDatabase with `DefaultServices` already provided.
+ */
+export const makeWithDefaults = <TRelations extends AnyRelations = EmptyRelations>(
+  config: EffectDrizzleSQLiteConfig<TRelations> = {},
+) => make(config).pipe(Effect.provide(DefaultServices))

+ 4 - 0
packages/effect-drizzle-sqlite/src/effect-sqlite/index.ts

@@ -0,0 +1,4 @@
+/* oxlint-disable */
+export { EffectLogger } from "drizzle-orm/effect-core"
+export * from "./driver"
+export * from "./session"

+ 14 - 0
packages/effect-drizzle-sqlite/src/effect-sqlite/migrator.ts

@@ -0,0 +1,14 @@
+/* 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"
+import type { EffectSQLiteDatabase } from "./driver"
+
+export function migrate<TRelations extends AnyRelations>(
+  db: EffectSQLiteDatabase<TRelations>,
+  config: MigrationConfig,
+) {
+  const migrations = readMigrationFiles(config)
+  return coreMigrate(migrations, db.session, config)
+}

+ 214 - 0
packages/effect-drizzle-sqlite/src/effect-sqlite/session.ts

@@ -0,0 +1,214 @@
+/* oxlint-disable */
+import * as Context from "effect/Context"
+import * as Effect from "effect/Effect"
+import * as Exit from "effect/Exit"
+import * as Scope from "effect/Scope"
+import type { SqlClient } from "effect/unstable/sql/SqlClient"
+import type { SqlError } from "effect/unstable/sql/SqlError"
+import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
+import type { WithCacheConfig } from "drizzle-orm/cache/core/types"
+import type { EffectDrizzleQueryError } from "drizzle-orm/effect-core/errors"
+import type { EffectLoggerShape } from "drizzle-orm/effect-core/logger"
+import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind } from "drizzle-orm/entity"
+import type { AnyRelations } from "drizzle-orm/relations"
+import type { RelationalQueryMapperConfig } from "drizzle-orm/relations"
+import type { Query } from "drizzle-orm/sql/sql"
+import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+import { SQLiteEffectPreparedQuery, SQLiteEffectSession, SQLiteEffectTransaction } from "../sqlite-core/effect/session"
+import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { PreparedQueryConfig, SQLiteExecuteMethod, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
+
+export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
+  readonly error: EffectDrizzleQueryError
+  readonly context: never
+}
+
+export type EffectSQLiteRunResult = readonly never[]
+
+export interface EffectSQLiteSessionOptions {
+  logger: EffectLoggerShape
+  cache: EffectCacheShape
+  useJitMappers?: boolean
+}
+
+export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLiteEffectSession<
+  EffectSQLiteQueryEffectHKT,
+  EffectSQLiteRunResult,
+  TRelations
+> {
+  static override readonly [entityKind]: string = "EffectSQLiteSession"
+
+  constructor(
+    private client: SqlClient,
+    dialect: SQLiteAsyncDialect,
+    protected relations: TRelations,
+    private options: EffectSQLiteSessionOptions,
+  ) {
+    super(dialect)
+  }
+
+  override prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+    query: Query,
+    fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
+    queryMetadata?: {
+      type: "select" | "update" | "delete" | "insert"
+      tables: string[]
+    },
+    cacheConfig?: WithCacheConfig,
+  ): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT> {
+    return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT>(
+      (params, method) => this.execute(query, params, method),
+      query,
+      this.options.logger,
+      this.options.cache,
+      queryMetadata,
+      cacheConfig,
+      fields,
+      executeMethod,
+      this.options.useJitMappers,
+      customResultMapper,
+      undefined,
+      undefined,
+      this.isInTransaction(),
+    )
+  }
+
+  override prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+    query: Query,
+    fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
+    config: RelationalQueryMapperConfig,
+  ): SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true> {
+    return new SQLiteEffectPreparedQuery<T, EffectSQLiteQueryEffectHKT, true>(
+      (params, method) => this.execute(query, params, method),
+      query,
+      this.options.logger,
+      this.options.cache,
+      undefined,
+      undefined,
+      fields,
+      executeMethod,
+      this.options.useJitMappers,
+      customResultMapper,
+      true,
+      config,
+      this.isInTransaction(),
+    )
+  }
+
+  private execute(query: Query, params: unknown[], method: SQLiteExecuteMethod | "values") {
+    const statement = this.client.unsafe(query.sql, params)
+    if (method === "values") return statement.values
+    if (method === "get") return statement.withoutTransform.pipe(Effect.map((rows) => rows[0]))
+    return statement.withoutTransform
+  }
+
+  private isInTransaction() {
+    return Effect.serviceOption(this.client.transactionService).pipe(Effect.map((option) => option._tag === "Some"))
+  }
+
+  private executeTransactionStatement(connection: Effect.Success<SqlClient["reserve"]>, query: string) {
+    return connection.executeUnprepared(query, [], undefined).pipe(Effect.asVoid)
+  }
+
+  private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
+    return Effect.uninterruptibleMask((restore) =>
+      Effect.withFiber<A, E | SqlError, R>((fiber) => {
+        const services = fiber.context
+        const connectionOption = Context.getOption(services, this.client.transactionService)
+        const connection: Effect.Effect<
+          readonly [Scope.Closeable | undefined, Effect.Success<SqlClient["reserve"]>],
+          SqlError
+        > =
+          connectionOption._tag === "Some"
+            ? Effect.succeed([undefined, connectionOption.value[0]] as const)
+            : Scope.make().pipe(
+                Effect.flatMap((scope) =>
+                  Scope.provide(this.client.reserve, scope).pipe(
+                    Effect.map((connection) => [scope, connection] as const),
+                    Effect.catch((error) =>
+                      Scope.close(scope, Exit.fail(error)).pipe(Effect.andThen(Effect.fail(error))),
+                    ),
+                  ),
+                ),
+              )
+        const id = connectionOption._tag === "Some" ? connectionOption.value[1] + 1 : 0
+
+        return connection.pipe(
+          Effect.flatMap(([scope, connection]) =>
+            this.executeTransactionStatement(
+              connection,
+              id === 0 ? `begin ${config?.behavior ?? "deferred"}` : `savepoint effect_sql_${id}`,
+            ).pipe(
+              Effect.flatMap(() =>
+                Effect.provideContext(
+                  restore(effect),
+                  Context.add(services, this.client.transactionService, [connection, id]),
+                ),
+              ),
+              Effect.exit,
+              Effect.flatMap((exit) => {
+                const finalize = Exit.isSuccess(exit)
+                  ? id === 0
+                    ? this.executeTransactionStatement(connection, "commit").pipe(
+                        // SQLite keeps the transaction open after deferred constraint commit failures.
+                        Effect.catch((error) =>
+                          this.executeTransactionStatement(connection, "rollback").pipe(
+                            Effect.catch(() => Effect.void),
+                            Effect.andThen(Effect.fail(error)),
+                          ),
+                        ),
+                      )
+                    : this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`)
+                  : id === 0
+                    ? this.executeTransactionStatement(connection, "rollback")
+                    : this.executeTransactionStatement(connection, `rollback to savepoint effect_sql_${id}`).pipe(
+                        Effect.andThen(
+                          this.executeTransactionStatement(connection, `release savepoint effect_sql_${id}`),
+                        ),
+                      )
+                const scoped = scope === undefined ? finalize : Effect.ensuring(finalize, Scope.close(scope, exit))
+
+                return scoped.pipe(Effect.flatMap(() => exit))
+              }),
+            ),
+          ),
+        )
+      }),
+    )
+  }
+
+  override transaction<A, E, R>(
+    transaction: (tx: EffectSQLiteTransaction<TRelations>) => Effect.Effect<A, E, R>,
+    config?: SQLiteTransactionConfig,
+  ): Effect.Effect<A, E | SqlError, R> {
+    const { dialect, relations } = this
+
+    return this.withTransaction(
+      Effect.gen({ self: this }, function* () {
+        const tx = new EffectSQLiteTransaction<TRelations>(dialect, this, relations)
+
+        return yield* transaction(tx)
+      }),
+      config,
+    )
+  }
+}
+
+export class EffectSQLiteTransaction<TRelations extends AnyRelations> extends SQLiteEffectTransaction<
+  EffectSQLiteQueryEffectHKT,
+  EffectSQLiteRunResult,
+  TRelations
+> {
+  static override readonly [entityKind]: string = "EffectSQLiteTransaction"
+
+  override transaction: <A, E, R>(
+    transaction: (
+      tx: SQLiteEffectTransaction<EffectSQLiteQueryEffectHKT, EffectSQLiteRunResult, TRelations>,
+    ) => Effect.Effect<A, E, R>,
+  ) => Effect.Effect<A, SqlError | E, R> = (tx) => this.session.transaction(tx)
+}

+ 6 - 0
packages/effect-drizzle-sqlite/src/index.ts

@@ -0,0 +1,6 @@
+export { EffectLogger } from "drizzle-orm/effect-core"
+export * from "./effect-sqlite/driver"
+export * from "./effect-sqlite/session"
+export { migrate } from "./effect-sqlite/migrator"
+
+export * as EffectDrizzleSqlite from "."

+ 127 - 0
packages/effect-drizzle-sqlite/src/internal/drizzle-utils.ts

@@ -0,0 +1,127 @@
+/* oxlint-disable */
+import { Column, getColumnTable } from "drizzle-orm/column"
+import { is } from "drizzle-orm/entity"
+import type { JoinNullability } from "drizzle-orm/query-builders/select.types"
+import { Param, SQL } from "drizzle-orm/sql/sql"
+import type { SelectedFieldsOrdered } from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
+import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+import { Subquery } from "drizzle-orm/subquery"
+import { Table, getTableName } from "drizzle-orm/table"
+import type { UpdateSet } from "drizzle-orm/utils"
+import { ViewBaseConfig } from "drizzle-orm/view-common"
+
+const TableSymbol = (
+  Table as unknown as {
+    Symbol: { Columns: symbol; IsAlias: symbol; Name: symbol; BaseName: symbol }
+  }
+).Symbol
+
+export function getTableColumnsRuntime(table: SQLiteTable) {
+  return (table as unknown as Record<symbol, Record<string, Column>>)[TableSymbol.Columns]
+}
+
+export function getViewSelectedFieldsRuntime(view: SQLiteViewBase) {
+  return (view as unknown as Record<symbol, { selectedFields: Record<string, unknown>; name: string }>)[ViewBaseConfig]
+}
+
+export function jitCompatCheck(isEnabled: boolean | undefined) {
+  if (!isEnabled) return false
+  try {
+    return new Function("input", '"use strict"; return input;')(true) === true
+  } catch {
+    return false
+  }
+}
+
+export function orderSelectedFields<TColumn extends Column>(
+  fields: Record<string, unknown>,
+  pathPrefix?: string[],
+): SelectedFieldsOrdered {
+  return Object.entries(fields).flatMap(([name, field]) => {
+    const path = pathPrefix ? [...pathPrefix, name] : [name]
+    if (is(field, Column) || is(field, SQL) || is(field, SQL.Aliased) || is(field, Subquery)) {
+      return [{ path, field }] as SelectedFieldsOrdered
+    }
+    if (is(field, Table)) return orderSelectedFields(getTableColumnsRuntime(field as SQLiteTable), path)
+    return orderSelectedFields(field as Record<string, unknown>, path)
+  }) as SelectedFieldsOrdered
+}
+
+export function mapUpdateSet<TTable extends SQLiteTable>(table: TTable, values: SQLiteUpdateSetSource<TTable>) {
+  const entries = Object.entries(values).filter(([, value]) => value !== undefined)
+  if (entries.length === 0) throw new Error("No values to set")
+
+  return Object.fromEntries(
+    entries.map(([key, value]) => [
+      key,
+      is(value, SQL) || is(value, Column) ? value : new Param(value, getTableColumnsRuntime(table)[key]),
+    ]),
+  ) as UpdateSet
+}
+
+export function mapResultRow(
+  columns: SelectedFieldsOrdered,
+  row: unknown[],
+  joinsNotNullableMap: Record<string, boolean> | undefined,
+) {
+  const nullifyMap: Record<string, string | false> = {}
+  const result: Record<string, unknown> = {}
+
+  columns.forEach((column, columnIndex) => {
+    const decoder = (
+      is(column.field, Column)
+        ? column.field
+        : is(column.field, SQL)
+          ? (column.field as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
+          : is(column.field, Subquery)
+            ? (column.field._.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
+            : (column.field.sql as unknown as { decoder: { mapFromDriverValue(value: unknown): unknown } }).decoder
+    ) as {
+      mapFromDriverValue(value: unknown): unknown
+    }
+    const rawValue = row[columnIndex]
+    const value = rawValue === null ? null : decoder.mapFromDriverValue(rawValue)
+    const objectName = column.path[0]
+    let node = result
+
+    column.path.forEach((pathChunk, pathChunkIndex) => {
+      if (pathChunkIndex === column.path.length - 1) {
+        node[pathChunk] = value
+        return
+      }
+      node[pathChunk] = (node[pathChunk] ?? {}) as Record<string, unknown>
+      node = node[pathChunk] as Record<string, unknown>
+    })
+
+    if (joinsNotNullableMap && is(column.field, Column) && column.path.length === 2 && objectName) {
+      const tableName = getTableName(getColumnTable(column.field))
+      nullifyMap[objectName] =
+        !(objectName in nullifyMap) && value === null
+          ? tableName
+          : typeof nullifyMap[objectName] === "string" && nullifyMap[objectName] !== tableName
+            ? false
+            : nullifyMap[objectName]
+    }
+  })
+
+  Object.entries(nullifyMap).forEach(([objectName, tableName]) => {
+    if (typeof tableName === "string" && !joinsNotNullableMap?.[tableName]) result[objectName] = null
+  })
+
+  return result
+}
+
+export function getTableLikeName(table: SQLiteTable | Subquery | SQLiteViewBase | SQL) {
+  if (is(table, Subquery)) return table._.alias
+  if (is(table, SQLiteViewBase)) return getViewSelectedFieldsRuntime(table).name
+  if (is(table, SQL)) return undefined
+  return (table as unknown as Record<symbol, string | boolean>)[
+    (table as unknown as Record<symbol, string | boolean>)[TableSymbol.IsAlias]
+      ? TableSymbol.Name
+      : TableSymbol.BaseName
+  ] as string
+}
+
+export type { JoinNullability }

+ 58 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/count.ts

@@ -0,0 +1,58 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind } from "drizzle-orm/entity"
+import { SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
+import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import type { SQLiteView } from "drizzle-orm/sqlite-core/view"
+import type { SQLiteEffectSession } from "./session"
+
+function buildSQLiteEmbeddedCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
+  return sql<number>`(select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters})`
+}
+
+function buildSQLiteCount(source: SQLiteTable | SQLiteView | SQL | SQLWrapper, filters?: SQL<unknown>) {
+  return sql<number>`select count(*) from ${source}${sql.raw(" where ").if(filters)}${filters}`
+}
+
+export interface SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+  extends SQL<number>,
+    SQLWrapper<number>,
+    Effect.Effect<number, TEffectHKT["error"], TEffectHKT["context"]> {}
+
+export class SQLiteEffectCountBuilder<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase> extends SQL<number> {
+  static override readonly [entityKind]: string = "SQLiteEffectCountBuilder"
+
+  private sql: SQL<number>
+  private session: SQLiteEffectSession<TEffectHKT, any, any>
+
+  constructor(params: {
+    source: SQLiteTable | SQLiteView | SQL | SQLWrapper
+    filters?: SQL<unknown>
+    session: SQLiteEffectSession<TEffectHKT, any, any>
+  }) {
+    super(buildSQLiteEmbeddedCount(params.source, params.filters).queryChunks)
+
+    this.session = params.session
+    this.sql = buildSQLiteCount(params.source, params.filters)
+  }
+
+  execute(placeholderValues?: Record<string, unknown>) {
+    return this.session
+      .prepareQuery<{
+        type: "async"
+        execute: number
+        run: unknown
+        all: unknown
+        get: unknown
+        values: unknown
+      }>(this.session.dialect.sqlToQuery(this.sql), undefined, "all", (rows) => {
+        const v = rows[0]?.[0]
+        if (typeof v === "number") return v
+        return v ? Number(v) : 0
+      })
+      .execute(placeholderValues)
+  }
+}
+
+applyEffectWrapper(SQLiteEffectCountBuilder)

+ 296 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/db.ts

@@ -0,0 +1,296 @@
+/* oxlint-disable */
+import { Effect } from "effect"
+import type { SqlError } from "effect/unstable/sql/SqlError"
+import type { EffectCacheShape } from "drizzle-orm/cache/core/cache-effect"
+import type { MutationOption } from "drizzle-orm/cache/core/cache"
+import type { QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind } from "drizzle-orm/entity"
+import type { TypedQueryBuilder } from "drizzle-orm/query-builders/query-builder"
+import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
+import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
+import { type ColumnsSelection, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
+import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder"
+import type { SelectedFields } from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
+import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import type { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+import { WithSubquery } from "drizzle-orm/subquery"
+import type { WithBuilder } from "drizzle-orm/sqlite-core/subquery"
+import { SQLiteEffectCountBuilder } from "./count"
+import { SQLiteEffectDeleteBase } from "./delete"
+import { SQLiteEffectInsertBuilder } from "./insert"
+import { SQLiteEffectRelationalQueryBuilder } from "./query"
+import { SQLiteEffectRaw } from "./raw"
+import { SQLiteEffectSelectBuilder } from "./select"
+import type { SQLiteEffectSelectBase } from "./select"
+import type { SQLiteEffectSession, SQLiteEffectTransaction } from "./session"
+import { SQLiteEffectUpdateBuilder } from "./update"
+
+export class SQLiteEffectDatabase<
+  TEffectHKT extends QueryEffectHKTBase,
+  TRunResult,
+  TRelations extends AnyRelations = EmptyRelations,
+> {
+  static readonly [entityKind]: string = "SQLiteEffectDatabase"
+
+  declare readonly _: {
+    readonly relations: TRelations
+    readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>
+  }
+
+  query: {
+    [K in keyof TRelations]: SQLiteEffectRelationalQueryBuilder<TRelations, TRelations[K], TEffectHKT>
+  }
+
+  constructor(
+    /** @internal */
+    readonly dialect: SQLiteAsyncDialect,
+    /** @internal */
+    readonly session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>,
+    relations: TRelations,
+    readonly rowModeRQB?: boolean,
+    readonly forbidJsonb?: boolean,
+  ) {
+    this._ = {
+      relations,
+      session,
+    }
+
+    this.query = {} as (typeof this)["query"]
+    for (const [tableName, relation] of Object.entries(relations)) {
+      ;(this.query as SQLiteEffectDatabase<TEffectHKT, TRunResult, AnyRelations>["query"])[tableName] =
+        new SQLiteEffectRelationalQueryBuilder(
+          relations,
+          relations[relation.name]!.table as SQLiteTable,
+          relation,
+          dialect,
+          session,
+          rowModeRQB,
+          forbidJsonb,
+        )
+    }
+
+    this.$cache = {
+      invalidate: (_params: MutationOption) => Effect.void,
+    }
+  }
+
+  $with: WithBuilder = (alias: string, selection?: ColumnsSelection) => {
+    const self = this
+    const as = (
+      qb:
+        | TypedQueryBuilder<ColumnsSelection | undefined>
+        | SQL
+        | ((qb: QueryBuilder) => TypedQueryBuilder<ColumnsSelection | undefined> | SQL),
+    ) => {
+      if (typeof qb === "function") {
+        qb = qb(new QueryBuilder(self.dialect))
+      }
+
+      return new Proxy(
+        new WithSubquery(
+          qb.getSQL(),
+          selection ??
+            (("getSelectedFields" in qb
+              ? ((qb as { getSelectedFields(): SelectedFields | undefined }).getSelectedFields() ?? {})
+              : {}) as SelectedFields),
+          alias,
+          true,
+        ),
+        new SelectionProxyHandler({ alias, sqlAliasedBehavior: "alias", sqlBehavior: "error" }),
+      )
+    }
+    return { as }
+  }
+
+  $cache: { invalidate: EffectCacheShape["onMutate"] }
+
+  $count(source: SQLiteTable | SQLiteViewBase | SQL | SQLWrapper, filters?: SQL<unknown>) {
+    return new SQLiteEffectCountBuilder({ source, filters, session: this.session })
+  }
+
+  with(...queries: WithSubquery[]) {
+    const self = this
+
+    function select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+    function select<TSelection extends SelectedFields>(
+      fields: TSelection,
+    ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+    function select(
+      fields?: SelectedFields,
+    ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+      return new SQLiteEffectSelectBuilder({
+        fields: fields ?? undefined,
+        session: self.session,
+        dialect: self.dialect,
+        withList: queries,
+      })
+    }
+
+    function selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+    function selectDistinct<TSelection extends SelectedFields>(
+      fields: TSelection,
+    ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+    function selectDistinct(
+      fields?: SelectedFields,
+    ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+      return new SQLiteEffectSelectBuilder({
+        fields: fields ?? undefined,
+        session: self.session,
+        dialect: self.dialect,
+        withList: queries,
+        distinct: true,
+      })
+    }
+
+    function update<TTable extends SQLiteTable>(
+      table: TTable,
+    ): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> {
+      return new SQLiteEffectUpdateBuilder(table, self.session, self.dialect, queries)
+    }
+
+    function insert<TTable extends SQLiteTable>(
+      into: TTable,
+    ): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> {
+      return new SQLiteEffectInsertBuilder(into, self.session, self.dialect, queries)
+    }
+
+    function delete_<TTable extends SQLiteTable>(
+      from: TTable,
+    ): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+      return new SQLiteEffectDeleteBase(from, self.session, self.dialect, queries)
+    }
+
+    return { select, selectDistinct, update, insert, delete: delete_ }
+  }
+
+  select(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+  select<TSelection extends SelectedFields>(
+    fields: TSelection,
+  ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+  select(fields?: SelectedFields): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+    return new SQLiteEffectSelectBuilder({ fields: fields ?? undefined, session: this.session, dialect: this.dialect })
+  }
+
+  selectDistinct(): SQLiteEffectSelectBuilder<undefined, TRunResult, TEffectHKT>
+  selectDistinct<TSelection extends SelectedFields>(
+    fields: TSelection,
+  ): SQLiteEffectSelectBuilder<TSelection, TRunResult, TEffectHKT>
+  selectDistinct(
+    fields?: SelectedFields,
+  ): SQLiteEffectSelectBuilder<SelectedFields | undefined, TRunResult, TEffectHKT> {
+    return new SQLiteEffectSelectBuilder({
+      fields: fields ?? undefined,
+      session: this.session,
+      dialect: this.dialect,
+      distinct: true,
+    })
+  }
+
+  update<TTable extends SQLiteTable>(table: TTable): SQLiteEffectUpdateBuilder<TTable, TRunResult, TEffectHKT> {
+    return new SQLiteEffectUpdateBuilder(table, this.session, this.dialect)
+  }
+
+  insert<TTable extends SQLiteTable>(into: TTable): SQLiteEffectInsertBuilder<TTable, TRunResult, TEffectHKT> {
+    return new SQLiteEffectInsertBuilder(into, this.session, this.dialect)
+  }
+
+  delete<TTable extends SQLiteTable>(
+    from: TTable,
+  ): SQLiteEffectDeleteBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+    return new SQLiteEffectDeleteBase(from, this.session, this.dialect)
+  }
+
+  private raw<TResult>(
+    query: SQLWrapper | string,
+    action: "all" | "get" | "run" | "values",
+    execute: (query: SQL) => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+  ): SQLiteEffectRaw<TResult, TEffectHKT> {
+    const sequel = typeof query === "string" ? sql.raw(query) : query.getSQL()
+    return new SQLiteEffectRaw(
+      () => execute(sequel),
+      () => sequel,
+      action,
+      this.dialect,
+      (result) => result,
+    )
+  }
+
+  run(query: SQLWrapper | string): SQLiteEffectRaw<TRunResult, TEffectHKT> {
+    return this.raw(query, "run", (sequel) => this.session.run(sequel))
+  }
+
+  all<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> {
+    return this.raw(query, "all", (sequel) => this.session.all(sequel))
+  }
+
+  get<T = unknown>(query: SQLWrapper | string): SQLiteEffectRaw<T | undefined, TEffectHKT> {
+    return this.raw(query, "get", (sequel) => this.session.get(sequel))
+  }
+
+  values<T extends unknown[] = unknown[]>(query: SQLWrapper | string): SQLiteEffectRaw<T[], TEffectHKT> {
+    return this.raw(query, "values", (sequel) => this.session.values(sequel))
+  }
+
+  transaction: <A, E, R>(
+    transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>,
+    config?: SQLiteTransactionConfig,
+  ) => Effect.Effect<A, E | SqlError, R> = (tx, config) => this.session.transaction(tx, config)
+}
+
+export type SQLiteEffectWithReplicas<Q> = Q & { $primary: Q; $replicas: Q[] }
+
+export const withReplicas = <
+  TEffectHKT extends QueryEffectHKTBase,
+  TRunResult,
+  TRelations extends AnyRelations,
+  Q extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations>,
+>(
+  primary: Q,
+  replicas: [Q, ...Q[]],
+  getReplica: (replicas: Q[]) => Q = () => replicas[Math.floor(Math.random() * replicas.length)]!,
+): SQLiteEffectWithReplicas<Q> => {
+  const select: Q["select"] = (...args: []) => getReplica(replicas).select(...args)
+  const selectDistinct: Q["selectDistinct"] = (...args: []) => getReplica(replicas).selectDistinct(...args)
+  const $count: Q["$count"] = (...args: [any]) => getReplica(replicas).$count(...args)
+  const _with: Q["with"] = (...args: []) => getReplica(replicas).with(...args)
+  const $with = ((...args: [string] | [string, ColumnsSelection]) =>
+    args.length === 1
+      ? getReplica(replicas).$with(args[0])
+      : getReplica(replicas).$with(args[0], args[1])) as Q["$with"]
+
+  const update: Q["update"] = (...args: [any]) => primary.update(...args)
+  const insert: Q["insert"] = (...args: [any]) => primary.insert(...args)
+  const $delete: Q["delete"] = (...args: [any]) => primary.delete(...args)
+  const run: Q["run"] = (...args: [any]) => primary.run(...args)
+  const all: Q["all"] = (...args: [any]) => primary.all(...args)
+  const get: Q["get"] = (...args: [any]) => primary.get(...args)
+  const values: Q["values"] = (...args: [any]) => primary.values(...args)
+  const transaction: Q["transaction"] = (...args: [any]) => primary.transaction(...args)
+
+  return {
+    ...primary,
+    update,
+    insert,
+    delete: $delete,
+    run,
+    all,
+    get,
+    values,
+    transaction,
+    $primary: primary,
+    $replicas: replicas,
+    select,
+    selectDistinct,
+    $count,
+    $with,
+    with: _with,
+    get query() {
+      return getReplica(replicas).query
+    },
+  }
+}
+
+export type AnySQLiteEffectDatabase = SQLiteEffectDatabase<any, any, any>
+export type AnySQLiteEffectSelectBase = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any>

+ 261 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/delete.ts

@@ -0,0 +1,261 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind } from "drizzle-orm/entity"
+import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
+import type { RunnableQuery } from "drizzle-orm/runnable-query"
+import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
+import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
+import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+import type { SQLiteDeleteConfig } from "drizzle-orm/sqlite-core/query-builders/delete"
+import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
+import type { Subquery } from "drizzle-orm/subquery"
+import { type DrizzleTypeError, type ValueOrArray } from "drizzle-orm/utils"
+import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
+import { getTableColumnsRuntime, orderSelectedFields } from "../../internal/drizzle-utils"
+import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+
+export type SQLiteEffectDeleteWithout<
+  T extends AnySQLiteEffectDelete,
+  TDynamic extends boolean,
+  K extends keyof T & string,
+> = TDynamic extends true
+  ? T
+  : Omit<
+      SQLiteEffectDeleteBase<
+        T["_"]["table"],
+        T["_"]["runResult"],
+        T["_"]["returning"],
+        TDynamic,
+        T["_"]["excludedMethods"] | K,
+        T["_"]["effectHKT"]
+      >,
+      T["_"]["excludedMethods"] | K
+    >
+
+export type SQLiteEffectDeleteReturningAll<
+  T extends AnySQLiteEffectDelete,
+  TDynamic extends boolean,
+> = SQLiteEffectDeleteWithout<
+  SQLiteEffectDeleteBase<
+    T["_"]["table"],
+    T["_"]["runResult"],
+    T["_"]["table"]["$inferSelect"],
+    T["_"]["dynamic"],
+    T["_"]["excludedMethods"],
+    T["_"]["effectHKT"]
+  >,
+  TDynamic,
+  "returning"
+>
+
+export type SQLiteEffectDeleteReturning<
+  T extends AnySQLiteEffectDelete,
+  TDynamic extends boolean,
+  TSelectedFields extends SelectedFieldsFlat,
+> = SQLiteEffectDeleteWithout<
+  SQLiteEffectDeleteBase<
+    T["_"]["table"],
+    T["_"]["runResult"],
+    SelectResultFields<TSelectedFields>,
+    T["_"]["dynamic"],
+    T["_"]["excludedMethods"],
+    T["_"]["effectHKT"]
+  >,
+  TDynamic,
+  "returning"
+>
+
+export type SQLiteEffectDeleteExecute<T extends AnySQLiteEffectDelete> = T["_"]["returning"] extends undefined
+  ? T["_"]["runResult"]
+  : T["_"]["returning"][]
+
+export type SQLiteEffectDeletePrepare<
+  T extends AnySQLiteEffectDelete,
+  TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
+> = SQLiteEffectPreparedQuery<
+  PreparedQueryConfig & {
+    run: T["_"]["runResult"]
+    all: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".all() cannot be used without .returning()">
+      : T["_"]["returning"][]
+    get: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".get() cannot be used without .returning()">
+      : T["_"]["returning"] | undefined
+    values: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".values() cannot be used without .returning()">
+      : any[][]
+    execute: SQLiteEffectDeleteExecute<T>
+  },
+  TEffectHKT
+>
+
+export type SQLiteEffectDeleteDynamic<T extends AnySQLiteEffectDelete> = SQLiteEffectDelete<
+  T["_"]["table"],
+  T["_"]["runResult"],
+  T["_"]["returning"],
+  T["_"]["effectHKT"]
+>
+
+export type SQLiteEffectDelete<
+  TTable extends SQLiteTable = SQLiteTable,
+  TRunResult = unknown,
+  TReturning extends Record<string, unknown> | undefined = undefined,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> = SQLiteEffectDeleteBase<TTable, TRunResult, TReturning, true, never, TEffectHKT>
+
+export type AnySQLiteEffectDelete = SQLiteEffectDeleteBase<any, any, any, any, any, any>
+
+export interface SQLiteEffectDeleteBase<
+  TTable extends SQLiteTable,
+  TRunResult,
+  TReturning extends Record<string, unknown> | undefined = undefined,
+  TDynamic extends boolean = false,
+  _TExcludedMethods extends string = never,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> extends RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
+    SQLWrapper,
+    Effect.Effect<
+      TReturning extends undefined ? TRunResult : TReturning[],
+      TEffectHKT["error"],
+      TEffectHKT["context"]
+    > {
+  readonly _: {
+    dialect: "sqlite"
+    readonly table: TTable
+    readonly resultType: "async"
+    readonly runResult: TRunResult
+    readonly returning: TReturning
+    readonly dynamic: TDynamic
+    readonly excludedMethods: _TExcludedMethods
+    readonly result: TReturning extends undefined ? TRunResult : TReturning[]
+    readonly effectHKT: TEffectHKT
+  }
+}
+
+export class SQLiteEffectDeleteBase<
+    TTable extends SQLiteTable,
+    TRunResult,
+    TReturning extends Record<string, unknown> | undefined = undefined,
+    TDynamic extends boolean = false,
+    _TExcludedMethods extends string = never,
+    TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  >
+  implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
+{
+  static readonly [entityKind]: string = "SQLiteEffectDelete"
+
+  /** @internal */
+  config: SQLiteDeleteConfig
+
+  constructor(
+    private table: TTable,
+    private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+    private effectDialect: SQLiteDialect,
+    withList?: Subquery[],
+  ) {
+    this.config = { table, withList }
+  }
+
+  where(where: SQL | undefined): SQLiteEffectDeleteWithout<this, TDynamic, "where"> {
+    this.config.where = where
+    return this as any
+  }
+
+  orderBy(
+    builder: (deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>,
+  ): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy">
+  orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy">
+  orderBy(
+    ...columns:
+      | [(deleteTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>]
+      | (SQLiteColumn | SQL | SQL.Aliased)[]
+  ): SQLiteEffectDeleteWithout<this, TDynamic, "orderBy"> {
+    if (typeof columns[0] === "function") {
+      const orderBy = columns[0](
+        new Proxy(
+          getTableColumnsRuntime(this.config.table),
+          new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }),
+        ) as any,
+      )
+
+      this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]
+      return this as any
+    }
+
+    this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]
+    return this as any
+  }
+
+  limit(limit: number | Placeholder): SQLiteEffectDeleteWithout<this, TDynamic, "limit"> {
+    this.config.limit = limit
+    return this as any
+  }
+
+  returning(): SQLiteEffectDeleteReturningAll<this, TDynamic>
+  returning<TSelectedFields extends SelectedFieldsFlat>(
+    fields: TSelectedFields,
+  ): SQLiteEffectDeleteReturning<this, TDynamic, TSelectedFields>
+  returning(
+    fields: SelectedFieldsFlat = getTableColumnsRuntime(this.table),
+  ): SQLiteEffectDeleteReturning<this, TDynamic, any> | SQLiteEffectDeleteReturningAll<this, TDynamic> {
+    this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
+    return this as any
+  }
+
+  /** @internal */
+  getSQL(): SQL {
+    return this.effectDialect.buildDeleteQuery(this.config)
+  }
+
+  toSQL(): Query {
+    return this.effectDialect.sqlToQuery(this.getSQL())
+  }
+
+  /** @internal */
+  _prepare(isOneTimeQuery = true): SQLiteEffectDeletePrepare<this, TEffectHKT> {
+    return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+      this.effectDialect.sqlToQuery(this.getSQL()),
+      this.config.returning,
+      this.config.returning ? "all" : "run",
+      undefined,
+      {
+        type: "delete",
+        tables: extractUsedTable(this.config.table),
+      },
+    ) as SQLiteEffectDeletePrepare<this, TEffectHKT>
+  }
+
+  prepare(): SQLiteEffectDeletePrepare<this, TEffectHKT> {
+    return this._prepare(false)
+  }
+
+  run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+    return this._prepare().run(placeholderValues)
+  }
+
+  all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+    return this._prepare().all(placeholderValues)
+  }
+
+  get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+    return this._prepare().get(placeholderValues)
+  }
+
+  values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+    return this._prepare().values(placeholderValues)
+  }
+
+  execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+    return this._prepare().execute(placeholderValues)
+  }
+
+  $dynamic(): SQLiteEffectDeleteDynamic<this> {
+    return this as any
+  }
+}
+
+applyEffectWrapper(SQLiteEffectDeleteBase)

+ 10 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/index.ts

@@ -0,0 +1,10 @@
+/* oxlint-disable */
+export * from "./count"
+export * from "./db"
+export * from "./delete"
+export * from "./insert"
+export * from "./query"
+export * from "./raw"
+export * from "./select"
+export * from "./session"
+export * from "./update"

+ 349 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/insert.ts

@@ -0,0 +1,349 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind, is } from "drizzle-orm/entity"
+import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
+import type { RunnableQuery } from "drizzle-orm/runnable-query"
+import type { Query, SQLWrapper } from "drizzle-orm/sql/sql"
+import { Param, SQL, sql } from "drizzle-orm/sql/sql"
+import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+import type { IndexColumn } from "drizzle-orm/sqlite-core/indexes"
+import type {
+  SQLiteInsertConfig,
+  SQLiteInsertSelectQueryBuilder,
+  SQLiteInsertValue,
+} from "drizzle-orm/sqlite-core/query-builders/insert"
+import type { SelectedFieldsFlat } from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
+import type { Subquery } from "drizzle-orm/subquery"
+import { type DrizzleTypeError, haveSameKeys } from "drizzle-orm/utils"
+import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
+import { QueryBuilder } from "drizzle-orm/sqlite-core/query-builders/query-builder"
+import type { SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
+import { getTableColumnsRuntime, mapUpdateSet, orderSelectedFields } from "../../internal/drizzle-utils"
+import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+
+export type SQLiteEffectInsertWithout<
+  T extends AnySQLiteEffectInsert,
+  TDynamic extends boolean,
+  K extends keyof T & string,
+> = TDynamic extends true
+  ? T
+  : Omit<
+      SQLiteEffectInsertBase<
+        T["_"]["table"],
+        T["_"]["runResult"],
+        T["_"]["returning"],
+        TDynamic,
+        T["_"]["excludedMethods"] | K,
+        T["_"]["effectHKT"]
+      >,
+      T["_"]["excludedMethods"] | K
+    >
+
+export type SQLiteEffectInsertReturning<
+  T extends AnySQLiteEffectInsert,
+  TDynamic extends boolean,
+  TSelectedFields extends SelectedFieldsFlat,
+> = SQLiteEffectInsertWithout<
+  SQLiteEffectInsertBase<
+    T["_"]["table"],
+    T["_"]["runResult"],
+    SelectResultFields<TSelectedFields>,
+    TDynamic,
+    T["_"]["excludedMethods"],
+    T["_"]["effectHKT"]
+  >,
+  TDynamic,
+  "returning"
+>
+
+export type SQLiteEffectInsertReturningAll<
+  T extends AnySQLiteEffectInsert,
+  TDynamic extends boolean,
+> = SQLiteEffectInsertWithout<
+  SQLiteEffectInsertBase<
+    T["_"]["table"],
+    T["_"]["runResult"],
+    T["_"]["table"]["$inferSelect"],
+    TDynamic,
+    T["_"]["excludedMethods"],
+    T["_"]["effectHKT"]
+  >,
+  TDynamic,
+  "returning"
+>
+
+export type SQLiteEffectInsertDynamic<T extends AnySQLiteEffectInsert> = SQLiteEffectInsert<
+  T["_"]["table"],
+  T["_"]["runResult"],
+  T["_"]["returning"],
+  T["_"]["effectHKT"]
+>
+
+export type SQLiteEffectInsertOnConflictDoUpdateConfig<T extends AnySQLiteEffectInsert> = {
+  target: IndexColumn | IndexColumn[]
+  /** @deprecated - use either `targetWhere` or `setWhere` */
+  where?: SQL
+  targetWhere?: SQL
+  setWhere?: SQL
+  set: SQLiteUpdateSetSource<T["_"]["table"]>
+}
+
+export type SQLiteEffectInsertExecute<T extends AnySQLiteEffectInsert> = T["_"]["returning"] extends undefined
+  ? T["_"]["runResult"]
+  : T["_"]["returning"][]
+
+export type SQLiteEffectInsertPrepare<
+  T extends AnySQLiteEffectInsert,
+  TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
+> = SQLiteEffectPreparedQuery<
+  PreparedQueryConfig & {
+    run: T["_"]["runResult"]
+    all: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".all() cannot be used without .returning()">
+      : T["_"]["returning"][]
+    get: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".get() cannot be used without .returning()">
+      : T["_"]["returning"]
+    values: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".values() cannot be used without .returning()">
+      : any[][]
+    execute: SQLiteEffectInsertExecute<T>
+  },
+  TEffectHKT
+>
+
+export type SQLiteEffectInsert<
+  TTable extends SQLiteTable = SQLiteTable,
+  TRunResult = unknown,
+  TReturning = any,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> = SQLiteEffectInsertBase<TTable, TRunResult, TReturning, true, never, TEffectHKT>
+
+export type AnySQLiteEffectInsert = SQLiteEffectInsertBase<any, any, any, any, any, any>
+
+export class SQLiteEffectInsertBuilder<
+  TTable extends SQLiteTable,
+  TRunResult,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> {
+  static readonly [entityKind]: string = "SQLiteEffectInsertBuilder"
+
+  constructor(
+    protected table: TTable,
+    protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+    protected dialect: SQLiteDialect,
+    private withList?: Subquery[],
+  ) {}
+
+  values(
+    value: SQLiteInsertValue<TTable>,
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+  values(
+    values: SQLiteInsertValue<TTable>[],
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+  values(
+    values: SQLiteInsertValue<TTable> | SQLiteInsertValue<TTable>[],
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+    values = Array.isArray(values) ? values : [values]
+    if (values.length === 0) {
+      throw new Error("values() must be called with at least one value")
+    }
+    const mappedValues = values.map((entry) => {
+      const result: Record<string, Param | SQL> = {}
+      const cols = getTableColumnsRuntime(this.table)
+      for (const colKey of Object.keys(entry)) {
+        const colValue = entry[colKey as keyof typeof entry]
+        result[colKey] = is(colValue, SQL) ? colValue : new Param(colValue, cols[colKey])
+      }
+      return result
+    })
+
+    return new SQLiteEffectInsertBase(this.table, mappedValues, this.session, this.dialect, this.withList)
+  }
+
+  select(
+    selectQuery: (qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable>,
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+  select(
+    selectQuery: (qb: QueryBuilder) => SQL,
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+  select(selectQuery: SQL): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+  select(
+    selectQuery: SQLiteInsertSelectQueryBuilder<TTable>,
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT>
+  select(
+    selectQuery:
+      | SQL
+      | SQLiteInsertSelectQueryBuilder<TTable>
+      | ((qb: QueryBuilder) => SQLiteInsertSelectQueryBuilder<TTable> | SQL),
+  ): SQLiteEffectInsertBase<TTable, TRunResult, undefined, false, never, TEffectHKT> {
+    const select = typeof selectQuery === "function" ? selectQuery(new QueryBuilder()) : selectQuery
+
+    if (!is(select, SQL) && !haveSameKeys(getTableColumnsRuntime(this.table), select._.selectedFields)) {
+      throw new Error(
+        "Insert select error: selected fields are not the same or are in a different order compared to the table definition",
+      )
+    }
+
+    return new SQLiteEffectInsertBase(this.table, select, this.session, this.dialect, this.withList, true)
+  }
+}
+
+export interface SQLiteEffectInsertBase<
+  TTable extends SQLiteTable,
+  TRunResult,
+  TReturning = undefined,
+  TDynamic extends boolean = false,
+  _TExcludedMethods extends string = never,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> extends SQLWrapper,
+    RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
+    Effect.Effect<
+      TReturning extends undefined ? TRunResult : TReturning[],
+      TEffectHKT["error"],
+      TEffectHKT["context"]
+    > {
+  readonly _: {
+    readonly dialect: "sqlite"
+    readonly table: TTable
+    readonly resultType: "async"
+    readonly runResult: TRunResult
+    readonly returning: TReturning
+    readonly dynamic: TDynamic
+    readonly excludedMethods: _TExcludedMethods
+    readonly result: TReturning extends undefined ? TRunResult : TReturning[]
+    readonly effectHKT: TEffectHKT
+  }
+}
+
+export class SQLiteEffectInsertBase<
+    TTable extends SQLiteTable,
+    TRunResult,
+    TReturning = undefined,
+    TDynamic extends boolean = false,
+    _TExcludedMethods extends string = never,
+    TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  >
+  implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
+{
+  static readonly [entityKind]: string = "SQLiteEffectInsert"
+
+  /** @internal */
+  config: SQLiteInsertConfig<TTable>
+
+  constructor(
+    private table: TTable,
+    values: SQLiteInsertConfig["values"],
+    private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+    private effectDialect: SQLiteDialect,
+    withList?: Subquery[],
+    select?: boolean,
+  ) {
+    this.config = { table, values: values as any, withList, select }
+  }
+
+  returning(): SQLiteEffectInsertReturningAll<this, TDynamic>
+  returning<TSelectedFields extends SelectedFieldsFlat>(
+    fields: TSelectedFields,
+  ): SQLiteEffectInsertReturning<this, TDynamic, TSelectedFields>
+  returning(
+    fields: SelectedFieldsFlat = getTableColumnsRuntime(this.config.table),
+  ): SQLiteEffectInsertWithout<AnySQLiteEffectInsert, TDynamic, "returning"> {
+    this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
+    return this as any
+  }
+
+  onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this {
+    if (!this.config.onConflict) this.config.onConflict = []
+
+    if (config.target === undefined) {
+      this.config.onConflict.push(sql` on conflict do nothing`)
+      return this
+    }
+
+    const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`
+    const whereSql = config.where ? sql` where ${config.where}` : sql``
+    this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`)
+    return this
+  }
+
+  onConflictDoUpdate(config: SQLiteEffectInsertOnConflictDoUpdateConfig<this>): this {
+    if (config.where && (config.targetWhere || config.setWhere)) {
+      throw new Error(
+        'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.',
+      )
+    }
+
+    if (!this.config.onConflict) this.config.onConflict = []
+
+    const whereSql = config.where ? sql` where ${config.where}` : undefined
+    const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined
+    const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined
+    const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`
+    const setSql = this.effectDialect.buildUpdateSet(
+      this.config.table,
+      mapUpdateSet(this.config.table, config.set as SQLiteUpdateSetSource<TTable>),
+    )
+    this.config.onConflict.push(
+      sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`,
+    )
+    return this
+  }
+
+  /** @internal */
+  getSQL(): SQL {
+    return this.effectDialect.buildInsertQuery(this.config)
+  }
+
+  toSQL(): Query {
+    return this.effectDialect.sqlToQuery(this.getSQL())
+  }
+
+  /** @internal */
+  _prepare(isOneTimeQuery = true): SQLiteEffectInsertPrepare<this, TEffectHKT> {
+    return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+      this.effectDialect.sqlToQuery(this.getSQL()),
+      this.config.returning,
+      this.config.returning ? "all" : "run",
+      undefined,
+      {
+        type: "insert",
+        tables: extractUsedTable(this.config.table),
+      },
+    ) as SQLiteEffectInsertPrepare<this, TEffectHKT>
+  }
+
+  prepare(): SQLiteEffectInsertPrepare<this, TEffectHKT> {
+    return this._prepare(false)
+  }
+
+  run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+    return this._prepare().run(placeholderValues)
+  }
+
+  all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+    return this._prepare().all(placeholderValues)
+  }
+
+  get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+    return this._prepare().get(placeholderValues)
+  }
+
+  values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+    return this._prepare().values(placeholderValues)
+  }
+
+  execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+    return this._prepare().execute(placeholderValues)
+  }
+
+  $dynamic(): SQLiteEffectInsertDynamic<this> {
+    return this as any
+  }
+}
+
+applyEffectWrapper(SQLiteEffectInsertBase)

+ 198 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/query.ts

@@ -0,0 +1,198 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind } from "drizzle-orm/entity"
+import {
+  type BuildQueryResult,
+  type BuildRelationalQueryResult,
+  type DBQueryConfig,
+  makeDefaultRqbMapper,
+  type TableRelationalConfig,
+  type TablesRelationalConfig,
+} from "drizzle-orm/relations"
+import type { RunnableQuery } from "drizzle-orm/runnable-query"
+import { type Query, type SQL, sql, type SQLWrapper } from "drizzle-orm/sql/sql"
+import type { KnownKeysOnly } from "drizzle-orm/utils"
+import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+
+export class SQLiteEffectRelationalQueryBuilder<
+  TSchema extends TablesRelationalConfig,
+  TFields extends TableRelationalConfig,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> {
+  static readonly [entityKind]: string = "SQLiteEffectRelationalQueryBuilderV2"
+
+  constructor(
+    private schema: TSchema,
+    private table: SQLiteTable,
+    private tableConfig: TableRelationalConfig,
+    private dialect: SQLiteDialect,
+    private session: SQLiteEffectSession<TEffectHKT, any, any>,
+    private rowMode?: boolean,
+    private forbidJsonb?: boolean,
+  ) {}
+
+  findMany<TConfig extends DBQueryConfig<"many", TSchema, TFields>>(
+    config?: KnownKeysOnly<TConfig, DBQueryConfig<"many", TSchema, TFields>>,
+  ): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig>[], TEffectHKT> {
+    return new SQLiteEffectRelationalQuery(
+      this.schema,
+      this.table,
+      this.tableConfig,
+      this.dialect,
+      this.session,
+      (config as DBQueryConfig<"many"> | undefined) ?? true,
+      "many",
+      this.rowMode,
+      this.forbidJsonb,
+    )
+  }
+
+  findFirst<TConfig extends DBQueryConfig<"one", TSchema, TFields>>(
+    config?: KnownKeysOnly<TConfig, DBQueryConfig<"one", TSchema, TFields>>,
+  ): SQLiteEffectRelationalQuery<BuildQueryResult<TSchema, TFields, TConfig> | undefined, TEffectHKT> {
+    return new SQLiteEffectRelationalQuery(
+      this.schema,
+      this.table,
+      this.tableConfig,
+      this.dialect,
+      this.session,
+      (config as DBQueryConfig<"one"> | undefined) ?? true,
+      "first",
+      this.rowMode,
+      this.forbidJsonb,
+    )
+  }
+}
+
+export interface SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+  extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+    RunnableQuery<TResult, "sqlite">,
+    SQLWrapper {}
+
+export class SQLiteEffectRelationalQuery<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+  implements RunnableQuery<TResult, "sqlite">, SQLWrapper
+{
+  static readonly [entityKind]: string = "SQLiteEffectRelationalQueryV2"
+
+  declare readonly _: {
+    readonly dialect: "sqlite"
+    readonly type: "async"
+    readonly result: TResult
+  }
+
+  /** @internal */
+  mode: "many" | "first"
+  /** @internal */
+  table: SQLiteTable
+
+  constructor(
+    private schema: TablesRelationalConfig,
+    table: SQLiteTable,
+    private tableConfig: TableRelationalConfig,
+    private dialect: SQLiteDialect,
+    private session: SQLiteEffectSession<TEffectHKT, any, any>,
+    private config: DBQueryConfig<"many" | "one"> | true,
+    mode: "many" | "first",
+    private rowMode?: boolean,
+    private forbidJsonb?: boolean,
+  ) {
+    this.mode = mode
+    this.table = table
+  }
+
+  /** @internal */
+  getSQL(): SQL {
+    return this._getQuery().sql
+  }
+
+  /** @internal */
+  _prepare(
+    isOneTimeQuery = true,
+  ): SQLiteEffectPreparedQuery<
+    PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
+    TEffectHKT,
+    true
+  > {
+    const { query, builtQuery } = this._toSQL()
+    const mapperConfig = {
+      isFirst: this.mode === "first",
+      parseJson: !this.rowMode,
+      parseJsonIfString: false,
+      rootJsonMappers: true,
+      selection: query.selection,
+    }
+
+    return this.session[isOneTimeQuery ? "prepareOneTimeRelationalQuery" : "prepareRelationalQuery"](
+      builtQuery,
+      undefined,
+      this.mode === "first" ? "get" : "all",
+      makeDefaultRqbMapper(mapperConfig),
+      mapperConfig,
+    ) as SQLiteEffectPreparedQuery<
+      PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
+      TEffectHKT,
+      true
+    >
+  }
+
+  prepare(): SQLiteEffectPreparedQuery<
+    PreparedQueryConfig & { all: TResult; get: TResult; execute: TResult },
+    TEffectHKT,
+    true
+  > {
+    return this._prepare(false)
+  }
+
+  private _getQuery() {
+    const jsonb = this.forbidJsonb ? sql`json` : sql`jsonb`
+
+    const query = this.dialect.buildRelationalQuery({
+      schema: this.schema,
+      table: this.table,
+      tableConfig: this.tableConfig,
+      queryConfig: this.config,
+      mode: this.mode,
+      isNested: this.rowMode,
+      jsonb,
+    })
+
+    if (this.rowMode) {
+      const jsonColumns = sql.join(
+        query.selection.map((s) => {
+          return sql`${sql.raw(this.dialect.escapeString(s.key))}, ${
+            s.selection ? sql`${jsonb}(${sql.identifier(s.key)})` : sql.identifier(s.key)
+          }`
+        }),
+        sql`, `,
+      )
+
+      query.sql = sql`select json_object(${jsonColumns}) as ${sql.identifier("r")} from (${query.sql}) as ${sql.identifier(
+        "t",
+      )}`
+    }
+
+    return query
+  }
+
+  private _toSQL(): { query: BuildRelationalQueryResult; builtQuery: Query } {
+    const query = this._getQuery()
+
+    const builtQuery = this.dialect.sqlToQuery(query.sql)
+
+    return { query, builtQuery }
+  }
+
+  toSQL(): Query {
+    return this._toSQL().builtQuery
+  }
+
+  execute(placeholderValues?: Record<string, unknown>) {
+    return this.mode === "first" ? this._prepare().get(placeholderValues) : this._prepare().all(placeholderValues)
+  }
+}
+
+applyEffectWrapper(SQLiteEffectRelationalQuery)

+ 49 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/raw.ts

@@ -0,0 +1,49 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind } from "drizzle-orm/entity"
+import type { RunnableQuery } from "drizzle-orm/runnable-query"
+import type { PreparedQuery } from "drizzle-orm/session"
+import type { Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
+import type { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core/dialect"
+
+type SQLiteEffectRawAction = "all" | "get" | "values" | "run"
+
+export interface SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+  extends Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+    RunnableQuery<TResult, "sqlite">,
+    SQLWrapper {}
+
+export class SQLiteEffectRaw<TResult, TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+  implements RunnableQuery<TResult, "sqlite">, SQLWrapper, PreparedQuery
+{
+  static readonly [entityKind]: string = "SQLiteEffectRaw"
+
+  declare readonly _: {
+    readonly dialect: "sqlite"
+    readonly result: TResult
+  }
+
+  constructor(
+    public execute: () => Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]>,
+    /** @internal */
+    public getSQL: () => SQL,
+    private action: SQLiteEffectRawAction,
+    private dialect: SQLiteAsyncDialect,
+    private mapBatchResult: (result: unknown) => unknown,
+  ) {}
+
+  getQuery(): Query & { method: SQLiteEffectRawAction } {
+    return { ...this.dialect.sqlToQuery(this.getSQL()), method: this.action }
+  }
+
+  mapResult(result: unknown, isFromBatch?: boolean) {
+    return isFromBatch ? this.mapBatchResult(result) : result
+  }
+
+  _prepare(): PreparedQuery {
+    return this
+  }
+}
+
+applyEffectWrapper(SQLiteEffectRaw)

+ 279 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/select.ts

@@ -0,0 +1,279 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import type { CacheConfig } from "drizzle-orm/cache/core/types"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind, is } from "drizzle-orm/entity"
+import type {
+  BuildSubquerySelection,
+  GetSelectTableName,
+  GetSelectTableSelection,
+  JoinNullability,
+  SelectMode,
+  SelectResult,
+} from "drizzle-orm/query-builders/select.types"
+import { SQL } from "drizzle-orm/sql/sql"
+import type { ColumnsSelection, SQLWrapper } from "drizzle-orm/sql/sql"
+import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns"
+import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+import { SQLiteSelectQueryBuilderBase } from "drizzle-orm/sqlite-core/query-builders/select"
+import type {
+  CreateSQLiteSelectFromBuilderMode,
+  SelectedFields,
+  SQLiteSelectConfig,
+  SQLiteSelectHKTBase,
+} from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+import { Subquery } from "drizzle-orm/subquery"
+import { type Assume, getTableColumns } from "drizzle-orm/utils"
+import { getViewSelectedFieldsRuntime, orderSelectedFields } from "../../internal/drizzle-utils"
+import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+
+export type SQLiteEffectSelectPrepare<
+  T extends AnySQLiteEffectSelect,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> = SQLiteEffectPreparedQuery<
+  {
+    type: "async"
+    run: T["_"]["runResult"]
+    all: T["_"]["result"]
+    get: T["_"]["result"][number] | undefined
+    values: any[][]
+    execute: T["_"]["result"]
+  },
+  TEffectHKT
+>
+
+export class SQLiteEffectSelectBuilder<
+  TSelection extends SelectedFields | undefined,
+  TRunResult,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  TBuilderMode extends "db" | "qb" = "db",
+> {
+  static readonly [entityKind]: string = "SQLiteEffectSelectBuilder"
+
+  private fields: TSelection
+  private session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined
+  private dialect: SQLiteDialect
+  private withList: Subquery[] | undefined
+  private distinct: boolean | undefined
+
+  constructor(config: {
+    fields: TSelection
+    session: SQLiteEffectSession<TEffectHKT, TRunResult, any> | undefined
+    dialect: SQLiteDialect
+    withList?: Subquery[]
+    distinct?: boolean
+  }) {
+    this.fields = config.fields
+    this.session = config.session
+    this.dialect = config.dialect
+    this.withList = config.withList
+    this.distinct = config.distinct
+  }
+
+  from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>(
+    source: TFrom,
+  ): TBuilderMode extends "db"
+    ? SQLiteEffectSelectBase<
+        GetSelectTableName<TFrom>,
+        TRunResult,
+        TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+        TSelection extends undefined ? "single" : "partial",
+        GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {},
+        false,
+        never,
+        SelectResult<
+          TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+          TSelection extends undefined ? "single" : "partial",
+          GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}
+        >[],
+        BuildSubquerySelection<
+          TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+          GetSelectTableName<TFrom> extends string ? Record<GetSelectTableName<TFrom>, "not-null"> : {}
+        >,
+        TEffectHKT
+      >
+    : CreateSQLiteSelectFromBuilderMode<
+        TBuilderMode,
+        GetSelectTableName<TFrom>,
+        "async",
+        TRunResult,
+        TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection,
+        TSelection extends undefined ? "single" : "partial"
+      > {
+    const isPartialSelect = !!this.fields
+
+    let fields: SelectedFields
+    if (this.fields) {
+      fields = this.fields
+    } else if (is(source, Subquery)) {
+      fields = Object.fromEntries(
+        Object.keys(source._.selectedFields).map((key) => [
+          key,
+          source[key as unknown as keyof typeof source] as unknown as SelectedFields[string],
+        ]),
+      )
+    } else if (is(source, SQLiteViewBase)) {
+      fields = getViewSelectedFieldsRuntime(source).selectedFields as SelectedFields
+    } else if (is(source, SQL)) {
+      fields = {}
+    } else {
+      fields = getTableColumns<SQLiteTable>(source)
+    }
+
+    return new SQLiteEffectSelectBase({
+      table: source,
+      fields,
+      isPartialSelect,
+      session: this.session as any,
+      dialect: this.dialect,
+      withList: this.withList,
+      distinct: this.distinct,
+    }) as any
+  }
+}
+
+export interface SQLiteEffectSelectHKT<TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase>
+  extends SQLiteSelectHKTBase {
+  _type: SQLiteEffectSelectBase<
+    this["tableName"],
+    this["runResult"],
+    Assume<this["selection"], ColumnsSelection>,
+    this["selectMode"],
+    Assume<this["nullabilityMap"], Record<string, JoinNullability>>,
+    this["dynamic"],
+    this["excludedMethods"],
+    Assume<this["result"], any[]>,
+    Assume<this["selectedFields"], ColumnsSelection>,
+    TEffectHKT
+  >
+}
+
+export interface SQLiteEffectSelectBase<
+  TTableName extends string | undefined,
+  TRunResult,
+  TSelection extends ColumnsSelection,
+  TSelectMode extends SelectMode = "single",
+  TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
+    ? Record<TTableName, "not-null">
+    : {},
+  TDynamic extends boolean = false,
+  TExcludedMethods extends string = never,
+  TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
+  TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> extends SQLiteSelectQueryBuilderBase<
+      SQLiteEffectSelectHKT<TEffectHKT>,
+      TTableName,
+      "async",
+      TRunResult,
+      TSelection,
+      TSelectMode,
+      TNullabilityMap,
+      TDynamic,
+      TExcludedMethods,
+      TResult,
+      TSelectedFields
+    >,
+    Effect.Effect<TResult, TEffectHKT["error"], TEffectHKT["context"]> {}
+
+export class SQLiteEffectSelectBase<
+    TTableName extends string | undefined,
+    TRunResult,
+    TSelection extends ColumnsSelection,
+    TSelectMode extends SelectMode = "single",
+    TNullabilityMap extends Record<string, JoinNullability> = TTableName extends string
+      ? Record<TTableName, "not-null">
+      : {},
+    TDynamic extends boolean = false,
+    TExcludedMethods extends string = never,
+    TResult extends any[] = SelectResult<TSelection, TSelectMode, TNullabilityMap>[],
+    TSelectedFields extends ColumnsSelection = BuildSubquerySelection<TSelection, TNullabilityMap>,
+    TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  >
+  extends SQLiteSelectQueryBuilderBase<
+    SQLiteEffectSelectHKT<TEffectHKT>,
+    TTableName,
+    "async",
+    TRunResult,
+    TSelection,
+    TSelectMode,
+    TNullabilityMap,
+    TDynamic,
+    TExcludedMethods,
+    TResult,
+    TSelectedFields
+  >
+  implements SQLWrapper
+{
+  static override readonly [entityKind]: string = "SQLiteEffectSelect"
+
+  private get effectConfig() {
+    return (this as unknown as { config: SQLiteSelectConfig }).config
+  }
+
+  /** @internal */
+  getSQL(): SQL {
+    return this.dialect.buildSelectQuery(this.effectConfig)
+  }
+
+  /** @internal */
+  _prepare(isOneTimeQuery = true): SQLiteEffectSelectPrepare<this, TEffectHKT> {
+    if (!this.session) {
+      throw new Error("Cannot execute a query on a query builder. Please use a database instance instead.")
+    }
+    const session = this.session as unknown as SQLiteEffectSession<TEffectHKT, TRunResult, any>
+    const query = session[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+      this.dialect.sqlToQuery(this.getSQL()),
+      orderSelectedFields<SQLiteColumn>(this.effectConfig.fields),
+      "all",
+      undefined,
+      {
+        type: "select",
+        tables: [...this.usedTables],
+      },
+      this.cacheConfig,
+    )
+    query.joinsNotNullableMap = this.joinsNotNullableMap
+    return query as ReturnType<this["prepare"]>
+  }
+
+  $withCache(config?: { config?: CacheConfig; tag?: string; autoInvalidate?: boolean } | false) {
+    this.cacheConfig =
+      config === undefined
+        ? { config: {}, enabled: true, autoInvalidate: true }
+        : config === false
+          ? { enabled: false }
+          : { enabled: true, autoInvalidate: true, ...config }
+    return this
+  }
+
+  prepare(): SQLiteEffectSelectPrepare<this, TEffectHKT> {
+    return this._prepare(false)
+  }
+
+  run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+    return this._prepare().run(placeholderValues)
+  }
+
+  all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+    return this._prepare().all(placeholderValues)
+  }
+
+  get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+    return this._prepare().get(placeholderValues)
+  }
+
+  values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+    return this._prepare().values(placeholderValues)
+  }
+
+  execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+    return this._prepare().execute(placeholderValues)
+  }
+}
+
+applyEffectWrapper(SQLiteEffectSelectBase)
+
+export type AnySQLiteEffectSelect = SQLiteEffectSelectBase<any, any, any, any, any, any, any, any, any, any>

+ 490 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/session.ts

@@ -0,0 +1,490 @@
+/* oxlint-disable */
+import * as Cause from "effect/Cause"
+import * as Effect from "effect/Effect"
+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,
+  RelationalQueryMapperConfig,
+  RelationalRowsMapper,
+} 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 type { SQLiteAsyncDialect } 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"
+import { assertUnreachable, makeJitQueryMapper, type RowsMapper } from "drizzle-orm/utils"
+import { mapResultRow } from "../../internal/drizzle-utils"
+import { SQLiteEffectDatabase } from "./db"
+
+type MigrationConfigWithInit = MigrationConfig & { init?: boolean }
+
+type SQLiteEffectExecuteMethod = SQLiteExecuteMethod | "values"
+
+export class SQLiteEffectPreparedQuery<
+  T extends PreparedQueryConfig,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  TIsRqbV2 extends boolean = false,
+> implements PreparedQuery
+{
+  static readonly [entityKind]: string = "SQLiteEffectPreparedQuery"
+
+  /** @internal */
+  joinsNotNullableMap?: Record<string, boolean>
+  private jitMapper?: RowsMapper<any> | RelationalRowsMapper<any>
+  private cacheConfig: WithCacheConfig | undefined
+  private effectExecuteMethod: SQLiteExecuteMethod
+
+  constructor(
+    private executor: (
+      params: unknown[],
+      executeMethod: SQLiteEffectExecuteMethod,
+    ) => Effect.Effect<unknown, unknown, unknown>,
+    protected query: Query,
+    private logger: EffectLoggerShape,
+    private cache: EffectCacheShape,
+    private queryMetadata:
+      | {
+          type: "select" | "update" | "delete" | "insert"
+          tables: string[]
+        }
+      | undefined,
+    cacheConfig: WithCacheConfig | undefined,
+    private fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    private useJitMappers: boolean | undefined,
+    private customResultMapper?: (
+      rows: TIsRqbV2 extends true ? Record<string, unknown>[] : unknown[][],
+      mapColumnValue?: (value: unknown) => unknown,
+    ) => unknown,
+    private isRqbV2Query?: TIsRqbV2,
+    private rqbConfig?: RelationalQueryMapperConfig,
+    private isInTransaction: Effect.Effect<boolean> = Effect.succeed(false),
+  ) {
+    this.effectExecuteMethod = executeMethod
+    this.cacheConfig =
+      cache.strategy() === "all" && cacheConfig === undefined ? { enabled: true, autoInvalidate: true } : cacheConfig
+    if (!this.cacheConfig?.enabled) {
+      this.cacheConfig = undefined
+    }
+  }
+
+  run(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["run"]>
+  run(placeholderValues?: Record<string, unknown>): any {
+    return this.executeWithCache<T["run"]>(placeholderValues, "run")
+  }
+
+  all(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["all"]>
+  all(placeholderValues?: Record<string, unknown>): any {
+    if (this.isRqbV2Query) return this.allRqbV2(placeholderValues)
+
+    if (!this.fields && !this.customResultMapper) {
+      return this.executeWithCache<T["all"]>(placeholderValues, "all")
+    }
+
+    return this.executeWithCache<T["values"], T["all"]>(
+      placeholderValues,
+      "values",
+      (rows) => this.mapAllResult(rows) as T["all"],
+    )
+  }
+
+  get(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["get"]>
+  get(placeholderValues?: Record<string, unknown>): any {
+    if (this.isRqbV2Query) return this.getRqbV2(placeholderValues)
+
+    if (!this.fields && !this.customResultMapper) {
+      return this.executeWithCache<T["get"]>(placeholderValues, "get")
+    }
+
+    return this.executeWithCache<T["values"], T["get"]>(
+      placeholderValues,
+      "values",
+      (rows) => this.mapGetResult(rows) as T["get"],
+    )
+  }
+
+  values(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["values"]>
+  values(placeholderValues?: Record<string, unknown>): any {
+    return this.executeWithCache<T["values"]>(placeholderValues, "values")
+  }
+
+  execute(placeholderValues?: Record<string, unknown>): QueryEffectKind<TEffectHKT, T["execute"]>
+  execute(placeholderValues?: Record<string, unknown>): any {
+    return this[this.effectExecuteMethod](placeholderValues) as QueryEffectKind<TEffectHKT, T["execute"]>
+  }
+
+  mapRunResult(result: unknown, _isFromBatch?: boolean): unknown {
+    return result
+  }
+
+  mapAllResult(rows: unknown, isFromBatch?: boolean): unknown {
+    if (isFromBatch) {
+      rows = Array.isArray(rows) ? rows : []
+    }
+
+    if (!this.fields && !this.customResultMapper) {
+      return rows
+    }
+
+    if (this.isRqbV2Query) {
+      return this.useJitMappers
+        ? (this.jitMapper =
+            (this.jitMapper as RelationalRowsMapper<T["all"]>) ?? makeJitRqbMapper<T["all"]>(this.rqbConfig!))(
+            rows as Record<string, unknown>[],
+          )
+        : (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)(rows as Record<string, unknown>[])
+    }
+
+    if (this.customResultMapper) {
+      return (this.customResultMapper as (rows: unknown[][]) => unknown)(rows as unknown[][]) as T["all"]
+    }
+
+    return this.useJitMappers
+      ? (this.jitMapper =
+          (this.jitMapper as RowsMapper<T["all"]>) ??
+          makeJitQueryMapper<T["all"]>(this.fields!, this.joinsNotNullableMap))(rows as unknown[][])
+      : (rows as unknown[][]).map((row) => mapResultRow(this.fields!, row, this.joinsNotNullableMap))
+  }
+
+  mapGetResult(rows: unknown, isFromBatch?: boolean): unknown {
+    if (isFromBatch) {
+      rows = Array.isArray(rows) ? rows : []
+    }
+
+    if (!this.fields && !this.customResultMapper) {
+      return Array.isArray(rows) ? rows[0] : rows
+    }
+
+    const row = Array.isArray(rows) ? rows[0] : rows
+    if (!row) return undefined
+
+    if (this.isRqbV2Query) {
+      return this.useJitMappers
+        ? (this.jitMapper =
+            (this.jitMapper as RelationalRowsMapper<T["get"][]>) ?? makeJitRqbMapper<T["get"][]>(this.rqbConfig!))([
+            row as Record<string, unknown>,
+          ])
+        : (this.customResultMapper as (rows: Record<string, unknown>[]) => unknown)([row as Record<string, unknown>])
+    }
+
+    if (this.customResultMapper) {
+      return (this.customResultMapper as (rows: unknown[][]) => unknown)([row as unknown[]]) as T["get"]
+    }
+
+    return this.useJitMappers
+      ? (this.jitMapper =
+          (this.jitMapper as RowsMapper<T["get"][]>) ??
+          makeJitQueryMapper<T["get"][]>(this.fields!, this.joinsNotNullableMap))([row as unknown[]])[0]
+      : mapResultRow(this.fields!, row as unknown[], this.joinsNotNullableMap)
+  }
+
+  private allRqbV2(placeholderValues?: Record<string, unknown>) {
+    return this.executeWithCache<unknown[], T["all"]>(
+      placeholderValues,
+      "all",
+      (rows) => this.mapAllResult(rows) as T["all"],
+    )
+  }
+
+  private getRqbV2(placeholderValues?: Record<string, unknown>) {
+    return this.executeWithCache<unknown, T["get"] | undefined>(placeholderValues, "get", (row) =>
+      row === undefined ? undefined : (this.mapGetResult(row) as T["get"]),
+    )
+  }
+
+  private executeWithCache<A, B = A>(
+    placeholderValues: Record<string, unknown> | undefined,
+    executeMethod: SQLiteEffectExecuteMethod,
+    mapResult?: (result: A) => B,
+  ) {
+    return Effect.gen({ self: this }, function* () {
+      const params = fillPlaceholders(this.query.params, placeholderValues ?? {})
+
+      yield* this.logger.logQuery(this.query.sql, params)
+
+      return yield* this.queryWithCache(
+        this.query.sql,
+        params,
+        Effect.suspend(() => this.executor(params, executeMethod) as Effect.Effect<A, unknown, unknown>),
+        mapResult,
+      )
+    })
+  }
+
+  private mapCachedResult<A, B>(result: A, mapResult: ((result: A) => B) | undefined) {
+    if (!mapResult) return Effect.succeed(result as unknown as B)
+    return Effect.try({
+      try: () => mapResult(result),
+      catch: (cause) => cause,
+    })
+  }
+
+  private queryWithCache<A, E, R, B = A>(
+    queryString: string,
+    params: unknown[],
+    query: Effect.Effect<A, E, R>,
+    mapResult?: (result: A) => B,
+  ) {
+    return Effect.gen({ self: this }, function* () {
+      if (this.queryMetadata?.type === "select" && this.cacheConfig?.enabled && (yield* this.isInTransaction)) {
+        return yield* this.mapCachedResult(yield* query, mapResult)
+      }
+
+      const cacheStrat: Awaited<ReturnType<typeof strategyFor>> = !is(this.cache.cache, NoopCache)
+        ? yield* Effect.tryPromise(() => strategyFor(queryString, params, this.queryMetadata, this.cacheConfig))
+        : { type: "skip" as const }
+
+      if (cacheStrat.type === "skip") {
+        return yield* this.mapCachedResult(yield* query, mapResult)
+      }
+
+      if (cacheStrat.type === "invalidate") {
+        const result = yield* query
+        yield* this.cache.onMutate({ tables: cacheStrat.tables })
+        return yield* this.mapCachedResult(result, mapResult)
+      }
+
+      if (cacheStrat.type === "try") {
+        if (yield* this.isInTransaction) {
+          return yield* this.mapCachedResult(yield* query, mapResult)
+        }
+
+        const { tables, key, isTag, autoInvalidate, config } = cacheStrat
+        const fromCache: any[] | undefined = yield* this.cache.get(key, tables, isTag, autoInvalidate)
+
+        if (typeof fromCache !== "undefined") {
+          return yield* this.mapCachedResult(fromCache as unknown as A, mapResult)
+        }
+
+        const result = yield* query
+
+        yield* this.cache.put(key, result, autoInvalidate ? tables : [], isTag, config)
+
+        return yield* this.mapCachedResult(result, mapResult)
+      }
+
+      assertUnreachable(cacheStrat)
+    }).pipe(
+      Effect.catch((e) => {
+        return Effect.fail(new EffectDrizzleQueryError({ query: queryString, params, cause: Cause.fail(e) }))
+      }),
+    )
+  }
+
+  getQuery(): Query {
+    return this.query
+  }
+
+  mapResult(response: unknown, isFromBatch?: boolean) {
+    switch (this.effectExecuteMethod) {
+      case "run": {
+        return this.mapRunResult(response, isFromBatch)
+      }
+      case "all": {
+        return this.mapAllResult(response, isFromBatch)
+      }
+      case "get": {
+        return this.mapGetResult(response, isFromBatch)
+      }
+    }
+  }
+}
+
+export abstract class SQLiteEffectSession<
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  TRunResult = unknown,
+  TRelations extends AnyRelations = EmptyRelations,
+> {
+  static readonly [entityKind]: string = "SQLiteEffectSession"
+
+  constructor(readonly dialect: SQLiteAsyncDialect) {}
+
+  abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+    query: Query,
+    fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
+    queryMetadata?: {
+      type: "select" | "update" | "delete" | "insert"
+      tables: string[]
+    },
+    cacheConfig?: WithCacheConfig,
+  ): SQLiteEffectPreparedQuery<T, TEffectHKT>
+
+  prepareOneTimeQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+    query: Query,
+    fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => unknown,
+    queryMetadata?: {
+      type: "select" | "update" | "delete" | "insert"
+      tables: string[]
+    },
+    cacheConfig?: WithCacheConfig,
+  ): SQLiteEffectPreparedQuery<T, TEffectHKT> {
+    return this.prepareQuery(query, fields, executeMethod, customResultMapper, queryMetadata, cacheConfig)
+  }
+
+  abstract prepareRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+    query: Query,
+    fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
+    config: RelationalQueryMapperConfig,
+  ): SQLiteEffectPreparedQuery<T, TEffectHKT, true>
+
+  prepareOneTimeRelationalQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
+    query: Query,
+    fields: SelectedFieldsOrdered | undefined,
+    executeMethod: SQLiteExecuteMethod,
+    customResultMapper: (rows: Record<string, unknown>[], mapColumnValue?: (value: unknown) => unknown) => unknown,
+    config: RelationalQueryMapperConfig,
+  ): SQLiteEffectPreparedQuery<T, TEffectHKT, true> {
+    return this.prepareRelationalQuery(query, fields, executeMethod, customResultMapper, config)
+  }
+
+  run(query: SQL): QueryEffectKind<TEffectHKT, TRunResult>
+  run(query: SQL): any {
+    return this.prepareQuery<PreparedQueryConfig & { run: TRunResult; execute: TRunResult }>(
+      this.dialect.sqlToQuery(query),
+      undefined,
+      "run",
+    ).run()
+  }
+
+  all<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T[]>
+  all<T = unknown>(query: SQL): any {
+    return this.prepareQuery<PreparedQueryConfig & { all: T[]; execute: T[] }>(
+      this.dialect.sqlToQuery(query),
+      undefined,
+      "all",
+    ).all()
+  }
+
+  get<T = unknown>(query: SQL): QueryEffectKind<TEffectHKT, T | undefined>
+  get<T = unknown>(query: SQL): any {
+    return this.prepareQuery<PreparedQueryConfig & { get: T | undefined; execute: T | undefined }>(
+      this.dialect.sqlToQuery(query),
+      undefined,
+      "get",
+    ).get()
+  }
+
+  values<T extends unknown[] = unknown[]>(query: SQL): QueryEffectKind<TEffectHKT, T[]>
+  values<T extends unknown[] = unknown[]>(query: SQL): any {
+    return this.prepareQuery<PreparedQueryConfig & { values: T[]; execute: T[] }>(
+      this.dialect.sqlToQuery(query),
+      undefined,
+      "all",
+    ).values()
+  }
+
+  count(query: SQL): QueryEffectKind<TEffectHKT, number>
+  count(query: SQL): any {
+    return this.values<[number]>(query).pipe(Effect.map((result) => result[0]?.[0] ?? 0))
+  }
+
+  abstract transaction<A, E, R>(
+    transaction: (tx: SQLiteEffectTransaction<TEffectHKT, TRunResult, TRelations>) => Effect.Effect<A, E, R>,
+    config?: SQLiteTransactionConfig,
+  ): Effect.Effect<A, E | SqlError, R>
+}
+
+export abstract class SQLiteEffectTransaction<
+  TEffectHKT extends QueryEffectHKTBase,
+  TRunResult,
+  TRelations extends AnyRelations = EmptyRelations,
+> extends SQLiteEffectDatabase<TEffectHKT, TRunResult, TRelations> {
+  static override readonly [entityKind]: string = "SQLiteEffectTransaction"
+
+  constructor(
+    dialect: SQLiteAsyncDialect,
+    session: SQLiteEffectSession<TEffectHKT, TRunResult, TRelations>,
+    protected relations: TRelations,
+  ) {
+    super(dialect, session, relations)
+  }
+
+  rollback() {
+    return new EffectTransactionRollbackError()
+  }
+}
+
+export const migrate = Effect.fn("migrate")(function* <TEffectHKT extends QueryEffectHKTBase>(
+  migrations: MigrationMeta[],
+  session: SQLiteEffectSession<TEffectHKT>,
+  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()})`,
+        )
+      }
+    }),
+  )
+})

+ 402 - 0
packages/effect-drizzle-sqlite/src/sqlite-core/effect/update.ts

@@ -0,0 +1,402 @@
+/* oxlint-disable */
+import type * as Effect from "effect/Effect"
+import { applyEffectWrapper, type QueryEffectHKTBase } from "drizzle-orm/effect-core/query-effect"
+import { entityKind, is } from "drizzle-orm/entity"
+import type { SelectResultFields } from "drizzle-orm/query-builders/select.types"
+import type { RunnableQuery } from "drizzle-orm/runnable-query"
+import { SelectionProxyHandler } from "drizzle-orm/selection-proxy"
+import type { Placeholder, Query, SQL, SQLWrapper } from "drizzle-orm/sql/sql"
+import type { SQLiteDialect } from "drizzle-orm/sqlite-core/dialect"
+import type { SelectedFields, SQLiteSelectJoinConfig } from "drizzle-orm/sqlite-core/query-builders/select.types"
+import type { SQLiteUpdateConfig, SQLiteUpdateSetSource } from "drizzle-orm/sqlite-core/query-builders/update"
+import type { PreparedQueryConfig } from "drizzle-orm/sqlite-core/session"
+import { SQLiteTable } from "drizzle-orm/sqlite-core/table"
+import { extractUsedTable } from "drizzle-orm/sqlite-core/utils"
+import { SQLiteViewBase } from "drizzle-orm/sqlite-core/view-base"
+import { Subquery } from "drizzle-orm/subquery"
+import { type DrizzleTypeError, type UpdateSet, type ValueOrArray } from "drizzle-orm/utils"
+import type { SQLiteColumn } from "drizzle-orm/sqlite-core/columns/common"
+import {
+  getTableColumnsRuntime,
+  getTableLikeName,
+  getViewSelectedFieldsRuntime,
+  mapUpdateSet,
+  orderSelectedFields,
+} from "../../internal/drizzle-utils"
+import type { SQLiteEffectPreparedQuery, SQLiteEffectSession } from "./session"
+
+export type SQLiteEffectUpdateWithout<
+  T extends AnySQLiteEffectUpdate,
+  TDynamic extends boolean,
+  K extends keyof T & string,
+> = TDynamic extends true
+  ? T
+  : Omit<
+      SQLiteEffectUpdateBase<
+        T["_"]["table"],
+        T["_"]["runResult"],
+        T["_"]["from"],
+        T["_"]["returning"],
+        TDynamic,
+        T["_"]["excludedMethods"] | K,
+        T["_"]["effectHKT"]
+      >,
+      T["_"]["excludedMethods"] | K
+    >
+
+export type SQLiteEffectUpdateWithJoins<
+  T extends AnySQLiteEffectUpdate,
+  TDynamic extends boolean,
+  TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL,
+> = TDynamic extends true
+  ? T
+  : Omit<
+      SQLiteEffectUpdateBase<
+        T["_"]["table"],
+        T["_"]["runResult"],
+        TFrom,
+        T["_"]["returning"],
+        TDynamic,
+        Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">,
+        T["_"]["effectHKT"]
+      >,
+      Exclude<T["_"]["excludedMethods"] | "from", "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin">
+    >
+
+export type SQLiteEffectUpdateReturningAll<
+  T extends AnySQLiteEffectUpdate,
+  TDynamic extends boolean,
+> = SQLiteEffectUpdateWithout<
+  SQLiteEffectUpdateBase<
+    T["_"]["table"],
+    T["_"]["runResult"],
+    T["_"]["from"],
+    T["_"]["table"]["$inferSelect"],
+    TDynamic,
+    T["_"]["excludedMethods"],
+    T["_"]["effectHKT"]
+  >,
+  TDynamic,
+  "returning"
+>
+
+export type SQLiteEffectUpdateReturning<
+  T extends AnySQLiteEffectUpdate,
+  TDynamic extends boolean,
+  TSelectedFields extends SelectedFields,
+> = SQLiteEffectUpdateWithout<
+  SQLiteEffectUpdateBase<
+    T["_"]["table"],
+    T["_"]["runResult"],
+    T["_"]["from"],
+    SelectResultFields<TSelectedFields>,
+    TDynamic,
+    T["_"]["excludedMethods"],
+    T["_"]["effectHKT"]
+  >,
+  TDynamic,
+  "returning"
+>
+
+export type SQLiteEffectUpdateExecute<T extends AnySQLiteEffectUpdate> = T["_"]["returning"] extends undefined
+  ? T["_"]["runResult"]
+  : T["_"]["returning"][]
+
+export type SQLiteEffectUpdatePrepare<
+  T extends AnySQLiteEffectUpdate,
+  TEffectHKT extends QueryEffectHKTBase = T["_"]["effectHKT"],
+> = SQLiteEffectPreparedQuery<
+  PreparedQueryConfig & {
+    run: T["_"]["runResult"]
+    all: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".all() cannot be used without .returning()">
+      : T["_"]["returning"][]
+    get: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".get() cannot be used without .returning()">
+      : T["_"]["returning"]
+    values: T["_"]["returning"] extends undefined
+      ? DrizzleTypeError<".values() cannot be used without .returning()">
+      : any[][]
+    execute: SQLiteEffectUpdateExecute<T>
+  },
+  TEffectHKT
+>
+
+export type SQLiteEffectUpdateDynamic<T extends AnySQLiteEffectUpdate> = SQLiteEffectUpdate<
+  T["_"]["table"],
+  T["_"]["runResult"],
+  T["_"]["from"],
+  T["_"]["returning"],
+  T["_"]["effectHKT"]
+>
+
+export type SQLiteEffectUpdate<
+  TTable extends SQLiteTable = SQLiteTable,
+  TRunResult = unknown,
+  TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
+  TReturning extends Record<string, unknown> | undefined = Record<string, unknown> | undefined,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> = SQLiteEffectUpdateBase<TTable, TRunResult, TFrom, TReturning, true, never, TEffectHKT>
+
+export type AnySQLiteEffectUpdate = SQLiteEffectUpdateBase<any, any, any, any, any, any, any>
+
+export type SQLiteEffectUpdateJoinFn<T extends AnySQLiteEffectUpdate> = <
+  TJoinedTable extends SQLiteTable | Subquery | SQLiteViewBase | SQL,
+>(
+  table: TJoinedTable,
+  on:
+    | ((
+        updateTable: T["_"]["table"]["_"]["columns"],
+        from: T["_"]["from"] extends SQLiteTable
+          ? T["_"]["from"]["_"]["columns"]
+          : T["_"]["from"] extends Subquery | SQLiteViewBase
+            ? T["_"]["from"]["_"]["selectedFields"]
+            : never,
+      ) => SQL | undefined)
+    | SQL
+    | undefined,
+) => T
+
+export class SQLiteEffectUpdateBuilder<
+  TTable extends SQLiteTable,
+  TRunResult,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> {
+  static readonly [entityKind]: string = "SQLiteEffectUpdateBuilder"
+
+  declare readonly _: {
+    readonly table: TTable
+  }
+
+  constructor(
+    protected table: TTable,
+    protected session: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+    protected dialect: SQLiteDialect,
+    private withList?: Subquery[],
+  ) {}
+
+  set(
+    values: SQLiteUpdateSetSource<TTable>,
+  ): SQLiteEffectUpdateWithout<
+    SQLiteEffectUpdateBase<TTable, TRunResult, undefined, undefined, false, never, TEffectHKT>,
+    false,
+    "leftJoin" | "rightJoin" | "innerJoin" | "fullJoin"
+  > {
+    return new SQLiteEffectUpdateBase(
+      this.table,
+      mapUpdateSet(this.table, values),
+      this.session,
+      this.dialect,
+      this.withList,
+    ) as any
+  }
+}
+
+export interface SQLiteEffectUpdateBase<
+  TTable extends SQLiteTable = SQLiteTable,
+  TRunResult = unknown,
+  TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
+  TReturning = undefined,
+  TDynamic extends boolean = false,
+  _TExcludedMethods extends string = never,
+  TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+> extends SQLWrapper,
+    RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">,
+    Effect.Effect<
+      TReturning extends undefined ? TRunResult : TReturning[],
+      TEffectHKT["error"],
+      TEffectHKT["context"]
+    > {
+  readonly _: {
+    readonly dialect: "sqlite"
+    readonly table: TTable
+    readonly resultType: "async"
+    readonly runResult: TRunResult
+    readonly from: TFrom
+    readonly returning: TReturning
+    readonly dynamic: TDynamic
+    readonly excludedMethods: _TExcludedMethods
+    readonly result: TReturning extends undefined ? TRunResult : TReturning[]
+    readonly effectHKT: TEffectHKT
+  }
+}
+
+export class SQLiteEffectUpdateBase<
+    TTable extends SQLiteTable = SQLiteTable,
+    TRunResult = unknown,
+    TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL | undefined = undefined,
+    TReturning = undefined,
+    TDynamic extends boolean = false,
+    _TExcludedMethods extends string = never,
+    TEffectHKT extends QueryEffectHKTBase = QueryEffectHKTBase,
+  >
+  implements RunnableQuery<TReturning extends undefined ? TRunResult : TReturning[], "sqlite">, SQLWrapper
+{
+  static readonly [entityKind]: string = "SQLiteEffectUpdate"
+
+  /** @internal */
+  config: SQLiteUpdateConfig
+
+  constructor(
+    table: TTable,
+    set: UpdateSet,
+    private effectSession: SQLiteEffectSession<TEffectHKT, TRunResult, any>,
+    private effectDialect: SQLiteDialect,
+    withList?: Subquery[],
+  ) {
+    this.config = { set, table, withList, joins: [] }
+  }
+
+  from<TFrom extends SQLiteTable | Subquery | SQLiteViewBase | SQL>(
+    source: TFrom,
+  ): SQLiteEffectUpdateWithJoins<this, TDynamic, TFrom> {
+    this.config.from = source
+    return this as any
+  }
+
+  private createJoin<TJoinType extends SQLiteSelectJoinConfig["joinType"]>(
+    joinType: TJoinType,
+  ): SQLiteEffectUpdateJoinFn<this> {
+    return ((
+      table: SQLiteTable | Subquery | SQLiteViewBase | SQL,
+      on: ((updateTable: TTable, from: TFrom) => SQL | undefined) | SQL | undefined,
+    ) => {
+      const tableName = getTableLikeName(table)
+
+      if (typeof tableName === "string" && this.config.joins.some((join) => join.alias === tableName)) {
+        throw new Error(`Alias "${tableName}" is already used in this query`)
+      }
+
+      if (typeof on === "function") {
+        const from = this.config.from
+          ? is(table, SQLiteTable)
+            ? getTableColumnsRuntime(table)
+            : is(table, Subquery)
+              ? table._.selectedFields
+              : is(table, SQLiteViewBase)
+                ? getViewSelectedFieldsRuntime(table).selectedFields
+                : undefined
+          : undefined
+        on = on(
+          new Proxy(
+            this.config.table._.columns,
+            new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" }),
+          ) as any,
+          from &&
+            (new Proxy(from, new SelectionProxyHandler({ sqlAliasedBehavior: "sql", sqlBehavior: "sql" })) as any),
+        )
+      }
+
+      this.config.joins.push({ on, table, joinType, alias: tableName })
+
+      return this as any
+    }) as any
+  }
+
+  leftJoin = this.createJoin("left")
+
+  rightJoin = this.createJoin("right")
+
+  innerJoin = this.createJoin("inner")
+
+  fullJoin = this.createJoin("full")
+
+  where(where: SQL | undefined): SQLiteEffectUpdateWithout<this, TDynamic, "where"> {
+    this.config.where = where
+    return this as any
+  }
+
+  orderBy(
+    builder: (updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>,
+  ): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy">
+  orderBy(...columns: (SQLiteColumn | SQL | SQL.Aliased)[]): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy">
+  orderBy(
+    ...columns:
+      | [(updateTable: TTable) => ValueOrArray<SQLiteColumn | SQL | SQL.Aliased>]
+      | (SQLiteColumn | SQL | SQL.Aliased)[]
+  ): SQLiteEffectUpdateWithout<this, TDynamic, "orderBy"> {
+    if (typeof columns[0] === "function") {
+      const orderBy = columns[0](
+        new Proxy(
+          getTableColumnsRuntime(this.config.table),
+          new SelectionProxyHandler({ sqlAliasedBehavior: "alias", sqlBehavior: "sql" }),
+        ) as any,
+      )
+
+      this.config.orderBy = Array.isArray(orderBy) ? orderBy : [orderBy]
+      return this as any
+    }
+
+    this.config.orderBy = columns as (SQLiteColumn | SQL | SQL.Aliased)[]
+    return this as any
+  }
+
+  limit(limit: number | Placeholder): SQLiteEffectUpdateWithout<this, TDynamic, "limit"> {
+    this.config.limit = limit
+    return this as any
+  }
+
+  returning(): SQLiteEffectUpdateReturningAll<this, TDynamic>
+  returning<TSelectedFields extends SelectedFields>(
+    fields: TSelectedFields,
+  ): SQLiteEffectUpdateReturning<this, TDynamic, TSelectedFields>
+  returning(
+    fields: SelectedFields = getTableColumnsRuntime(this.config.table),
+  ): SQLiteEffectUpdateWithout<AnySQLiteEffectUpdate, TDynamic, "returning"> {
+    this.config.returning = orderSelectedFields<SQLiteColumn>(fields)
+    return this as any
+  }
+
+  /** @internal */
+  getSQL(): SQL {
+    return this.effectDialect.buildUpdateQuery(this.config)
+  }
+
+  toSQL(): Query {
+    return this.effectDialect.sqlToQuery(this.getSQL())
+  }
+
+  /** @internal */
+  _prepare(isOneTimeQuery = true): SQLiteEffectUpdatePrepare<this, TEffectHKT> {
+    return this.effectSession[isOneTimeQuery ? "prepareOneTimeQuery" : "prepareQuery"](
+      this.effectDialect.sqlToQuery(this.getSQL()),
+      this.config.returning,
+      this.config.returning ? "all" : "run",
+      undefined,
+      {
+        type: "update",
+        tables: extractUsedTable(this.config.table),
+      },
+    ) as SQLiteEffectUpdatePrepare<this, TEffectHKT>
+  }
+
+  prepare(): SQLiteEffectUpdatePrepare<this, TEffectHKT> {
+    return this._prepare(false)
+  }
+
+  run: ReturnType<this["prepare"]>["run"] = (placeholderValues) => {
+    return this._prepare().run(placeholderValues)
+  }
+
+  all: ReturnType<this["prepare"]>["all"] = (placeholderValues) => {
+    return this._prepare().all(placeholderValues)
+  }
+
+  get: ReturnType<this["prepare"]>["get"] = (placeholderValues) => {
+    return this._prepare().get(placeholderValues)
+  }
+
+  values: ReturnType<this["prepare"]>["values"] = (placeholderValues) => {
+    return this._prepare().values(placeholderValues)
+  }
+
+  execute: ReturnType<this["prepare"]>["execute"] = (placeholderValues) => {
+    return this._prepare().execute(placeholderValues)
+  }
+
+  $dynamic(): SQLiteEffectUpdateDynamic<this> {
+    return this as any
+  }
+}
+
+applyEffectWrapper(SQLiteEffectUpdateBase)

+ 102 - 0
packages/effect-drizzle-sqlite/src/up-migrations/effect-sqlite.ts

@@ -0,0 +1,102 @@
+/* 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"
+import {
+  buildSQLiteMigrationBackfillStatements,
+  prepareSQLiteMigrationBackfill,
+  type SQLiteMigrationTableRow,
+} from "./sqlite"
+import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils"
+
+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: <TEffectHKT extends QueryEffectHKTBase>(
+  migrationsTable: string,
+  session: SQLiteEffectSession<TEffectHKT>,
+  localMigrations: MigrationMeta[],
+) => Effect.Effect<UpgradeResult, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> =
+  Effect.fn("upgradeIfNeeded")(function* <TEffectHKT extends QueryEffectHKTBase>(
+    migrationsTable: string,
+    session: SQLiteEffectSession<TEffectHKT>,
+    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,
+  <TEffectHKT extends QueryEffectHKTBase>(
+    migrationsTable: string,
+    session: SQLiteEffectSession<TEffectHKT>,
+    localMigrations: MigrationMeta[],
+  ) => Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]>
+> = {
+  0: upgradeFromV0,
+}
+
+function upgradeFromV0<TEffectHKT extends QueryEffectHKTBase>(
+  migrationsTable: string,
+  session: SQLiteEffectSession<TEffectHKT>,
+  localMigrations: MigrationMeta[],
+): Effect.Effect<void, EffectDrizzleError | TEffectHKT["error"] | SqlError, TEffectHKT["context"]> {
+  return Effect.gen(function* () {
+    const table = sql`${sql.identifier(migrationsTable)}`
+
+    const dbRows = yield* session.all<SQLiteMigrationTableRow>(
+      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)
+        }
+      }),
+    )
+  })
+}

+ 253 - 0
packages/effect-drizzle-sqlite/src/up-migrations/sqlite.ts

@@ -0,0 +1,253 @@
+/* oxlint-disable */
+import type { TablesRelationalConfig } from "drizzle-orm/_relations"
+import type { MigrationMeta } from "drizzle-orm/migrator"
+import type { AnyRelations } from "drizzle-orm/relations"
+import { type SQL, sql } from "drizzle-orm/sql/sql"
+import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core"
+import type { SQLiteSession } from "drizzle-orm/sqlite-core/session"
+import { GET_VERSION_FOR, MIGRATIONS_TABLE_VERSIONS, type UpgradeResult } from "./utils"
+
+/** @internal */
+export type SQLiteMigrationTableRow = { id: number | null; hash: string; created_at: number }
+
+type AsyncSQLiteDatabaseWithSession = BaseSQLiteDatabase<"async", unknown, Record<string, unknown>> & {
+  session: {
+    all<T>(query: SQL): Promise<T[]>
+  }
+  transaction<T>(transaction: (tx: { run(query: SQL): Promise<unknown> }) => Promise<T>): Promise<T>
+}
+
+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<number, MigrationMeta[]>()
+  const byHash = new Map<string, MigrationMeta>()
+  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
+}
+
+/**
+ * Detects the current version of the migrations table schema and upgrades it if needed.
+ *
+ * Version 0: Original schema (id, hash, created_at)
+ * Version 1: Extended schema (id, hash, created_at, name, applied_at)
+ */
+export function upgradeSyncIfNeeded(
+  migrationsTable: string,
+  session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>,
+  localMigrations: MigrationMeta[],
+): UpgradeResult {
+  const tableExists = session.all(sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`)
+
+  if (tableExists.length === 0) {
+    return { newDb: true }
+  }
+
+  // Table exists, check table shape
+  const rows = 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 = upgradeSyncFunctions[v]
+    if (!upgradeFn) {
+      throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`)
+    }
+    upgradeFn(migrationsTable, session, localMigrations)
+  }
+
+  return { newDb: false }
+}
+
+const upgradeSyncFunctions: Record<
+  number,
+  (
+    migrationsTable: string,
+    session: SQLiteSession<"sync", unknown, Record<string, unknown>, AnyRelations, TablesRelationalConfig>,
+    localMigrations: MigrationMeta[],
+  ) => void
+> = {
+  /**
+   * Upgrade from version 0 to version 1:
+   * 1. Read all existing DB migrations
+   * 2. Sort localMigrations ASC by millis and if the same - sort by name
+   * 3. Match each DB row to a local migration
+   * If multiple migrations share the same second, use hash matching as a tiebreaker
+   * Not implemented for now -> If hash matching fails, fall back to serial id ordering
+   * 5. Create extra column and backfill names for matched migrations
+   */
+  0: (migrationsTable, session, localMigrations) => {
+    const table = sql`${sql.identifier(migrationsTable)}`
+    const dbRows = session.all<SQLiteMigrationTableRow>(sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`)
+    const statements = buildSQLiteMigrationBackfillStatements(
+      migrationsTable,
+      prepareSQLiteMigrationBackfill(dbRows, localMigrations),
+    )
+
+    session.transaction((tx) => {
+      for (const statement of statements) {
+        tx.run(statement)
+      }
+    })
+  },
+}
+
+/**
+ * Detects the current version of the migrations table schema and upgrades it if needed.
+ *
+ * Version 0: Original schema (id, hash, created_at)
+ * Version 1: Extended schema (id, hash, created_at, name, applied_at)
+ */
+export async function upgradeAsyncIfNeeded(
+  migrationsTable: string,
+  db: AsyncSQLiteDatabaseWithSession,
+  localMigrations: MigrationMeta[],
+): Promise<UpgradeResult> {
+  // Check if the table exists at all
+  const tableExists = await db.session.all(
+    sql`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${migrationsTable}`,
+  )
+
+  if (tableExists.length === 0) {
+    return { newDb: true }
+  }
+
+  const rows = await db.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 = upgradeAsyncFunctions[v]
+    if (!upgradeFn) {
+      throw new Error(`No upgrade path from migration table version ${v} to ${v + 1}`)
+    }
+    await upgradeFn(migrationsTable, db, localMigrations)
+  }
+
+  return { newDb: false }
+}
+
+const upgradeAsyncFunctions: Record<
+  number,
+  (migrationsTable: string, db: AsyncSQLiteDatabaseWithSession, localMigrations: MigrationMeta[]) => Promise<void>
+> = {
+  /**
+   * Upgrade from version 0 to version 1:
+   * 1. Read all existing DB migrations
+   * 2. Sort localMigrations ASC by millis and if the same - sort by name
+   * 3. Match each DB row to a local migration
+   * If multiple migrations share the same second, use hash matching as a tiebreaker
+   * Not implemented for now -> If hash matching fails, fall back to serial id ordering
+   * 5. Create extra column and backfill names for matched migrations
+   */
+  0: async (migrationsTable, db, localMigrations) => {
+    const table = sql`${sql.identifier(migrationsTable)}`
+    const dbRows = await db.session.all<SQLiteMigrationTableRow>(
+      sql`SELECT id, hash, created_at FROM ${table} ORDER BY id ASC`,
+    )
+    const statements = buildSQLiteMigrationBackfillStatements(
+      migrationsTable,
+      prepareSQLiteMigrationBackfill(dbRows, localMigrations),
+    )
+
+    await db.transaction(async (tx) => {
+      for (const statement of statements) {
+        await tx.run(statement)
+      }
+    })
+  },
+}

+ 45 - 0
packages/effect-drizzle-sqlite/src/up-migrations/utils.ts

@@ -0,0 +1,45 @@
+/* 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

+ 139 - 0
packages/effect-drizzle-sqlite/test/sqlite.test.ts

@@ -0,0 +1,139 @@
+import { mkdir, mkdtemp, rm } from "node:fs/promises"
+import { tmpdir } from "node:os"
+import { join } from "node:path"
+import { expect, test } from "bun:test"
+import { SqliteClient } from "@effect/sql-sqlite-bun"
+import { eq, sql } from "drizzle-orm"
+import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
+import { Effect } from "effect"
+import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient"
+import { EffectDrizzleSqlite } from "../src"
+
+const users = sqliteTable("users", {
+  id: integer().primaryKey({ autoIncrement: true }),
+  name: text().notNull(),
+})
+
+const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
+  Effect.runPromise(
+    effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped),
+  )
+
+const makeDb = Effect.gen(function* () {
+  const db = yield* EffectDrizzleSqlite.makeWithDefaults()
+  yield* db.run(sql`create table users (id integer primary key autoincrement, name text not null)`)
+  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* () {
+      const db = yield* makeDb
+      yield* db.insert(users).values({ name: "Ada" })
+
+      expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Ada" }])
+      expect(yield* db.select({ id: users.id }).from(users).where(eq(users.name, "Ada")).get()).toEqual({ id: 1 })
+    }),
+  )
+})
+
+test("commits successful transactions", async () => {
+  await run(
+    Effect.gen(function* () {
+      const db = yield* makeDb
+
+      yield* db.transaction((tx) => tx.insert(users).values({ name: "Grace" }), { behavior: "immediate" })
+
+      expect(yield* db.select().from(users)).toEqual([{ id: 1, name: "Grace" }])
+    }),
+  )
+})
+
+test("rolls back failed transactions", async () => {
+  await run(
+    Effect.gen(function* () {
+      const db = yield* makeDb
+
+      yield* db
+        .transaction((tx) =>
+          tx
+            .insert(users)
+            .values({ name: "Linus" })
+            .pipe(Effect.andThen(Effect.fail("boom"))),
+        )
+        .pipe(Effect.ignore)
+
+      expect(yield* db.select().from(users)).toEqual([])
+    }),
+  )
+})
+
+test("rolls back explicit transaction rollback", async () => {
+  await run(
+    Effect.gen(function* () {
+      const db = yield* makeDb
+
+      yield* db
+        .transaction((tx) =>
+          tx
+            .insert(users)
+            .values({ name: "Barbara" })
+            .pipe(Effect.andThen(Effect.fail(tx.rollback()))),
+        )
+        .pipe(Effect.ignore)
+
+      expect(yield* db.select().from(users)).toEqual([])
+    }),
+  )
+})
+
+test("supports returning and rejects empty update sets", async () => {
+  await run(
+    Effect.gen(function* () {
+      const db = yield* makeDb
+
+      const inserted = yield* db.insert(users).values({ name: "Ada" }).returning({ id: users.id, name: users.name })
+      expect(inserted).toEqual([{ id: 1, name: "Ada" }])
+
+      const updated = yield* db.update(users).set({ name: "Grace" }).where(eq(users.id, 1)).returning()
+      expect(updated).toEqual([{ id: 1, name: "Grace" }])
+
+      const deleted = yield* db.delete(users).where(eq(users.id, 1)).returning({ id: users.id })
+      expect(deleted).toEqual([{ id: 1 }])
+
+      expect(() => db.update(users).set({ name: undefined })).toThrow("No values to set")
+    }),
+  )
+})
+
+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 })
+  }
+})

+ 15 - 0
packages/effect-drizzle-sqlite/tsconfig.json

@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@tsconfig/bun/tsconfig.json",
+  "compilerOptions": {
+    "lib": ["ESNext", "DOM", "DOM.Iterable"],
+    "noUncheckedIndexedAccess": false,
+    "plugins": [
+      {
+        "name": "@effect/language-service",
+        "transform": "@effect/language-service/transform",
+        "namespaceImportPackages": ["effect", "@effect/*"]
+      }
+    ]
+  }
+}

+ 145 - 0
specs/storage/effect-sqlite-package.md

@@ -0,0 +1,145 @@
+# Effect Drizzle SQLite Package
+
+## Goal
+
+Create a small workspace package that vendors the Drizzle `effect-sqlite` adapter shape for our repo. This is not an opencode storage abstraction. It is a local package that ports the Drizzle Effect SQLite implementation so we can use it before/independently of upstream release timing.
+
+`packages/opencode` will use it internally, but the package itself should be generic: Drizzle + Effect + SQLite. No opencode paths, migrations, tables, transaction hooks, post-commit behavior, or domain language should live in this package.
+
+## Package Shape
+
+Add a package similar in style to `packages/http-recorder`:
+
+- `packages/effect-drizzle-sqlite/package.json`
+- `packages/effect-drizzle-sqlite/src/index.ts`
+- `packages/effect-drizzle-sqlite/src/effect-sqlite/*`
+- `packages/effect-drizzle-sqlite/src/sqlite-core/effect/*`
+- `packages/effect-drizzle-sqlite/test/sqlite.test.ts`
+
+Package name:
+
+- `@opencode-ai/effect-drizzle-sqlite`
+
+Initial exports:
+
+```ts
+export { EffectLogger } from "drizzle-orm/effect-core"
+export * from "./effect-sqlite/driver"
+export * from "./effect-sqlite/session"
+export { migrate } from "./effect-sqlite/migrator"
+export * as EffectDrizzleSqlite from "."
+```
+
+The package should follow Drizzle's adapter naming and semantics as closely as possible. Think of it as a vendored `drizzle-orm/effect-sqlite` package surface, not as a new storage service API.
+
+## Upstream References
+
+Use these as implementation references instead of inventing a custom API:
+
+- Drizzle Effect Postgres current RC:
+  - `/Users/kit/code/open-source/drizzle-orm-rc4-pr/drizzle-orm/src/effect-core/query-effect.ts`
+  - `/Users/kit/code/open-source/drizzle-orm-rc4-pr/integration-tests/tests/pg/effect-sql.test.ts`
+- SQLite Effect branch/reference:
+  - `/Users/kit/code/open-source/drizzle-orm-beta16/drizzle-orm/src/up-migrations/effect-sqlite.ts`
+  - `/Users/kit/code/open-source/drizzle-orm-beta16/integration-tests/tests/sqlite/effect-sql.test.ts`
+  - `/Users/kit/code/open-source/drizzle-orm-beta16/drizzle-orm/type-tests/sqlite/effect.ts`
+- Effect SQLite client source of truth:
+  - `/Users/kit/code/open-source/effect-smol/packages/sql/sqlite-bun/src/SqliteClient.ts`
+  - `/Users/kit/code/open-source/effect-smol/packages/sql/sqlite-node/test/Client.test.ts`
+  - `/Users/kit/code/open-source/effect-smol/packages/sql/sqlite-node/test/SqliteMigrator.test.ts`
+
+Important API patterns from those references:
+
+- Drizzle queries are Effect-yieldable: `yield* db.select().from(table)`.
+- Transactions are Effect values: `yield* db.transaction((tx) => Effect.gen(...), { behavior: "immediate" })`.
+- SQLite clients come from Effect layers such as `SqliteClient.layer({ filename })`.
+- Migrations can run through Effect SQL/SQLite migrator mechanisms or Drizzle's `effect-sqlite/migrator` when available.
+
+## Public Surface
+
+Do not invent an `Interface<TDatabase>` abstraction unless the Drizzle port already has one. The public surface should mirror Drizzle's Effect adapters:
+
+```ts
+const db = yield * EffectDrizzleSqlite.make({ relations }).pipe(Effect.provide(EffectDrizzleSqlite.DefaultServices))
+
+yield * db.select().from(users)
+yield *
+  db.transaction(
+    (tx) =>
+      Effect.gen(function* () {
+        yield* tx.insert(users).values({ name: "Ada" })
+      }),
+    { behavior: "immediate" },
+  )
+```
+
+Notes:
+
+- `make` / `makeWithDefaults` should match the Drizzle Effect SQLite branch as much as possible.
+- `DefaultServices` should provide Drizzle's default logger/cache services, same as Effect Postgres.
+- The package should depend on Effect SQL SQLite clients (`@effect/sql-sqlite-bun` and/or node) the same way the Drizzle branch does.
+- Opencode-specific path/channel selection stays in `packages/opencode`.
+
+## Opencode Adoption Notes
+
+These are not package requirements, but they matter for the later opencode adoption PR.
+
+The current `packages/opencode/src/storage/db.ts` has two non-obvious semantics that the opencode wrapper must preserve when it consumes this adapter:
+
+- Nested `Database.use` inside `Database.transaction` sees the current transaction, not the root client.
+- `Database.effect` queues post-commit side effects while inside a transaction, and runs immediately outside a transaction.
+
+The opencode wrapper can implement that using Effect context instead of `LocalContext`:
+
+- A private transaction context holding `{ tx, afterCommit }`.
+- `withDb`/`db` methods read the current transaction context if present, otherwise use the root db.
+- `transaction` installs a transaction context around the effect.
+- Nested transactions can either reuse the existing tx initially, matching current behavior, or later use explicit savepoints if needed.
+
+Do not remove this behavior while moving opencode to Effect SQLite. `SyncEvent.run` depends on transaction composability and `behavior: "immediate"` for sequencing correctness.
+
+## Migration Strategy
+
+1. Add `@opencode-ai/effect-drizzle-sqlite` with a minimal in-memory/file SQLite test schema.
+2. Port the Drizzle Effect SQLite adapter from the SQLite branch into the package, preserving upstream names and API shape.
+3. Test adapter-level guarantees:
+   - query builders are yieldable Effect values,
+   - `transaction(..., { behavior: "immediate" })` commits successful writes,
+   - failed transaction rolls back,
+   - migrations run once and in order,
+   - close finalizer closes the underlying SQLite database.
+4. Add `@opencode-ai/effect-drizzle-sqlite` as a dependency of `packages/opencode`.
+5. Port `packages/opencode/src/storage/db.ts` to be a thin compatibility wrapper over the adapter plus opencode-specific transaction/post-commit context.
+6. Keep existing call sites working first:
+   - `Database.Client()`
+   - `Database.use(...)`
+   - `Database.transaction(...)`
+   - `Database.effect(...)`
+7. After compatibility is stable, migrate call sites from callback-style `Database.use` to yielding Effect Drizzle queries directly.
+8. Only then build domain stores like session/message/project stores on top of opencode's storage wrapper.
+
+## Why This Is Cleaner Than Starting With SessionStorage
+
+`SessionStorage` is a useful domain seam, but it does not answer the core adapter problem: how to make Drizzle SQLite Effect-native in this repo.
+
+An Effect Drizzle SQLite package lets us vendor the adapter once. Then opencode can build its own storage wrapper on top, and `SessionStorage`, `MessageStorage`, event store, and projector writes can all share the same transaction and migration model.
+
+## Open Questions
+
+- Which client should the first package target: `@effect/sql-sqlite-bun`, `@effect/sql-sqlite-node`, or both behind separate layers?
+- How much source should we copy from the Drizzle branch versus import from catalog `drizzle-orm` internals?
+- What is the update path once Drizzle upstream ships `effect-sqlite`?
+- Should `afterCommit` stay opencode-specific until event publishing moves? Default answer: yes.
+- Should the compatibility wrapper preserve synchronous return types temporarily, or should the migration intentionally force Effect call sites?
+- Do CLI/admin raw SQL and sqlite shell stay in `packages/opencode`, or does the storage package expose backend capabilities for them?
+
+## Recommended First PR
+
+Make the first PR package-only and intentionally boring:
+
+- Add `packages/effect-drizzle-sqlite`.
+- Use a tiny test schema, not opencode domain tables.
+- Prove Effect Drizzle SQLite queries, transactions, and migrations.
+- Do not migrate `packages/opencode` yet except possibly adding the dependency if needed for typechecking.
+
+That gives us a focused place to validate the Effect SQLite approach before disturbing opencode's current database runtime.