( db: D1Database, blobs: R2Bucket | undefined, )
| 85 | // --------------------------------------------------------------------------- |
| 86 | |
| 87 | export const createD1ExecutorDb = async ( |
| 88 | db: D1Database, |
| 89 | blobs: R2Bucket | undefined, |
| 90 | ): Promise<ExecutorDbHandle> => { |
| 91 | const options = { |
| 92 | tables: collectTables(), |
| 93 | namespace: CLOUDFLARE_NAMESPACE, |
| 94 | version: CLOUDFLARE_SCHEMA_VERSION, |
| 95 | provider: "sqlite" as const, |
| 96 | }; |
| 97 | |
| 98 | const schema = createDrizzleRuntimeSchemaFromTables(options); |
| 99 | const drizzleDb = drizzle(db, { schema }); |
| 100 | |
| 101 | // D1 rejects SQL `BEGIN TRANSACTION` / `SAVEPOINT` (it requires the JS batch |
| 102 | // API), and the shared ensure wraps its DDL in a transaction when the handle |
| 103 | // exposes one. A generated fingerprint lets every Worker/DO isolate prove the |
| 104 | // expected generated schema was already prepared with one primary read instead of |
| 105 | // replaying dozens of idempotent CREATE/ALTER statements on every database |
| 106 | // open. The marker is stored only after the ensure, data migrations, and any |
| 107 | // required post-compatibility ensure succeed. Data migrations retain their |
| 108 | // own ledger and still run below on every open. |
| 109 | const expectedFingerprint = await schemaFingerprint( |
| 110 | createDrizzleRuntimeSchemaSqlFromTables(options), |
| 111 | ); |
| 112 | const preparedFingerprint = await readPreparedSchemaFingerprint(db); |
| 113 | const requiresSchemaPreparation = preparedFingerprint !== expectedFingerprint; |
| 114 | if (requiresSchemaPreparation) { |
| 115 | await ensureDrizzleRuntimeSchemaFromTables({ run: (query) => drizzleDb.run(query) }, options); |
| 116 | } |
| 117 | const migrationResult = await prepareCloudflareD1Data(db, blobs); |
| 118 | if (requiresSchemaPreparation) { |
| 119 | // Compatibility migrations can rebuild a legacy table. Re-run the |
| 120 | // idempotent ensure before stamping so the database matches the generated |
| 121 | // schema after migration, including newer nullable columns the legacy |
| 122 | // rebuild did not know about. |
| 123 | if (migrationResult.schemaChanged) { |
| 124 | await ensureDrizzleRuntimeSchemaFromTables({ run: (query) => drizzleDb.run(query) }, options); |
| 125 | } |
| 126 | await storePreparedSchemaFingerprint(db, expectedFingerprint); |
| 127 | } |
| 128 | |
| 129 | // `interactiveTransactions: false` — D1 rejects interactive transactions, so |
| 130 | // the fuma adapter runs transaction callbacks directly (auto-commit per |
| 131 | // statement). Without this, every runtime write that wraps in a transaction |
| 132 | // (adding a source, etc.) emits `BEGIN` and 500s. libSQL keeps real |
| 133 | // transactions; D1 (same `provider: "sqlite"`) opts out here. |
| 134 | const { db: fumaDb, fuma } = createExecutorFumaDb(drizzleDb, { |
| 135 | ...options, |
| 136 | interactiveTransactions: false, |
| 137 | // D1 caps bound parameters at 100 per query; createMany batches to fit |
| 138 | // (otherwise a wide table like `tool` overflows with "too many SQL |
| 139 | // variables" when a source derives many tools). |
| 140 | maxBoundParameters: 100, |
| 141 | }); |
| 142 | |
| 143 | return { |
| 144 | db: fumaDb, |
no test coverage detected