( db: D1Database, blobs: R2Bucket | undefined, )
| 27 | // --------------------------------------------------------------------------- |
| 28 | |
| 29 | export const createD1ExecutorDb = async ( |
| 30 | db: D1Database, |
| 31 | blobs: R2Bucket | undefined, |
| 32 | ): Promise<ExecutorDbHandle> => { |
| 33 | const options = { |
| 34 | tables: collectTables(), |
| 35 | namespace: CLOUDFLARE_NAMESPACE, |
| 36 | version: CLOUDFLARE_SCHEMA_VERSION, |
| 37 | provider: "sqlite" as const, |
| 38 | }; |
| 39 | |
| 40 | const schema = createDrizzleRuntimeSchemaFromTables(options); |
| 41 | const drizzleDb = drizzle(db, { schema }); |
| 42 | |
| 43 | // D1 rejects SQL `BEGIN TRANSACTION` / `SAVEPOINT` (it requires the JS batch |
| 44 | // API), and the shared ensure wraps its DDL in a transaction when the handle |
| 45 | // exposes one. The bring-up is idempotent `CREATE TABLE IF NOT EXISTS`, so run |
| 46 | // it WITHOUT a transaction by handing the ensure a run-only view of the handle. |
| 47 | await ensureDrizzleRuntimeSchemaFromTables({ run: (query) => drizzleDb.run(query) }, options); |
| 48 | await runCloudflareDataMigrations(db, blobs); |
| 49 | |
| 50 | // `interactiveTransactions: false` — D1 rejects interactive transactions, so |
| 51 | // the fuma adapter runs transaction callbacks directly (auto-commit per |
| 52 | // statement). Without this, every runtime write that wraps in a transaction |
| 53 | // (adding a source, etc.) emits `BEGIN` and 500s. libSQL keeps real |
| 54 | // transactions; D1 (same `provider: "sqlite"`) opts out here. |
| 55 | const { db: fumaDb, fuma } = createExecutorFumaDb(drizzleDb, { |
| 56 | ...options, |
| 57 | interactiveTransactions: false, |
| 58 | // D1 caps bound parameters at 100 per query; createMany batches to fit |
| 59 | // (otherwise a wide table like `tool` overflows with "too many SQL |
| 60 | // variables" when a source derives many tools). |
| 61 | maxBoundParameters: 100, |
| 62 | }); |
| 63 | |
| 64 | return { |
| 65 | db: fumaDb, |
| 66 | fuma, |
| 67 | // The D1 binding owns its own lifecycle; nothing to release. |
| 68 | close: async () => {}, |
| 69 | // Multi-MB values (resolved OpenAPI specs, introspection snapshots) go |
| 70 | // through the blob seam straight to R2 — they never enter D1, which caps |
| 71 | // a value at ~1-2MB. Without a bucket bound, the executor falls back to |
| 72 | // the FumaDB blob table (small values only). |
| 73 | blobs: blobs ? makeR2BlobStore(blobs) : undefined, |
| 74 | }; |
| 75 | }; |
no test coverage detected