@drizzle-adapter/d1 1.1.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +22 -12
  2. package/dist/Adapter/D1DrizzleConnection.js +11 -8
  3. package/dist/Error/D1TransactionUnsupportedError.d.ts +10 -0
  4. package/dist/Error/D1TransactionUnsupportedError.js +13 -0
  5. package/dist/Interface/D1DrizzleClientInterface.d.ts +2 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/package.json +2 -1
  9. package/src/Adapter/D1DrizzleConnection.ts +14 -15
  10. package/src/Error/D1TransactionUnsupportedError.ts +17 -0
  11. package/src/Interface/D1DrizzleClientInterface.ts +2 -1
  12. package/src/index.ts +1 -0
  13. package/tests/Adapter/D1DrizzleConnection.test.ts +10 -0
  14. package/tests/Error/D1TransactionUnsupportedError.test.ts +12 -0
  15. package/tests/Integration/D1Atomicity.miniflare.test.ts +77 -0
  16. package/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/6e148680931b1bfcb993e0463aced3b0a6d6cf50af24dea2dcc2c3b2e9ae9e3b.sqlite +0 -0
  17. package/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/6e148680931b1bfcb993e0463aced3b0a6d6cf50af24dea2dcc2c3b2e9ae9e3b.sqlite-shm +0 -0
  18. package/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/6e148680931b1bfcb993e0463aced3b0a6d6cf50af24dea2dcc2c3b2e9ae9e3b.sqlite-wal +0 -0
  19. package/.wrangler/tmp/bundle-5POwQi/checked-fetch.js +0 -30
  20. package/.wrangler/tmp/bundle-5POwQi/middleware-insertion-facade.js +0 -11
  21. package/.wrangler/tmp/bundle-5POwQi/middleware-loader.entry.ts +0 -134
  22. package/.wrangler/tmp/bundle-6mwcNF/checked-fetch.js +0 -30
  23. package/.wrangler/tmp/bundle-6mwcNF/middleware-insertion-facade.js +0 -11
  24. package/.wrangler/tmp/bundle-6mwcNF/middleware-loader.entry.ts +0 -134
  25. package/.wrangler/tmp/bundle-Awe26S/checked-fetch.js +0 -30
  26. package/.wrangler/tmp/bundle-Awe26S/middleware-insertion-facade.js +0 -11
  27. package/.wrangler/tmp/bundle-Awe26S/middleware-loader.entry.ts +0 -134
  28. package/.wrangler/tmp/bundle-BJTmAe/checked-fetch.js +0 -30
  29. package/.wrangler/tmp/bundle-BJTmAe/middleware-insertion-facade.js +0 -11
  30. package/.wrangler/tmp/bundle-BJTmAe/middleware-loader.entry.ts +0 -134
  31. package/.wrangler/tmp/bundle-CHc9gh/checked-fetch.js +0 -30
  32. package/.wrangler/tmp/bundle-CHc9gh/middleware-insertion-facade.js +0 -11
  33. package/.wrangler/tmp/bundle-CHc9gh/middleware-loader.entry.ts +0 -134
  34. package/.wrangler/tmp/bundle-CHc9gh/strip-cf-connecting-ip-header.js +0 -13
  35. package/.wrangler/tmp/bundle-CHi1ri/checked-fetch.js +0 -30
  36. package/.wrangler/tmp/bundle-CHi1ri/middleware-insertion-facade.js +0 -11
  37. package/.wrangler/tmp/bundle-CHi1ri/middleware-loader.entry.ts +0 -134
  38. package/.wrangler/tmp/bundle-G5DpmU/checked-fetch.js +0 -30
  39. package/.wrangler/tmp/bundle-G5DpmU/middleware-insertion-facade.js +0 -11
  40. package/.wrangler/tmp/bundle-G5DpmU/middleware-loader.entry.ts +0 -134
  41. package/.wrangler/tmp/bundle-L3XGWN/checked-fetch.js +0 -30
  42. package/.wrangler/tmp/bundle-L3XGWN/middleware-insertion-facade.js +0 -11
  43. package/.wrangler/tmp/bundle-L3XGWN/middleware-loader.entry.ts +0 -134
  44. package/.wrangler/tmp/bundle-MycZGG/checked-fetch.js +0 -30
  45. package/.wrangler/tmp/bundle-MycZGG/middleware-insertion-facade.js +0 -11
  46. package/.wrangler/tmp/bundle-MycZGG/middleware-loader.entry.ts +0 -134
  47. package/.wrangler/tmp/bundle-PJMwtq/checked-fetch.js +0 -30
  48. package/.wrangler/tmp/bundle-PJMwtq/middleware-insertion-facade.js +0 -11
  49. package/.wrangler/tmp/bundle-PJMwtq/middleware-loader.entry.ts +0 -134
  50. package/.wrangler/tmp/bundle-SQq7Tu/checked-fetch.js +0 -30
  51. package/.wrangler/tmp/bundle-SQq7Tu/middleware-insertion-facade.js +0 -11
  52. package/.wrangler/tmp/bundle-SQq7Tu/middleware-loader.entry.ts +0 -134
  53. package/.wrangler/tmp/bundle-UNiTDE/checked-fetch.js +0 -30
  54. package/.wrangler/tmp/bundle-UNiTDE/middleware-insertion-facade.js +0 -11
  55. package/.wrangler/tmp/bundle-UNiTDE/middleware-loader.entry.ts +0 -134
  56. package/.wrangler/tmp/bundle-WqbqXV/checked-fetch.js +0 -30
  57. package/.wrangler/tmp/bundle-WqbqXV/middleware-insertion-facade.js +0 -11
  58. package/.wrangler/tmp/bundle-WqbqXV/middleware-loader.entry.ts +0 -134
  59. package/.wrangler/tmp/bundle-YhqsYC/checked-fetch.js +0 -30
  60. package/.wrangler/tmp/bundle-YhqsYC/middleware-insertion-facade.js +0 -11
  61. package/.wrangler/tmp/bundle-YhqsYC/middleware-loader.entry.ts +0 -134
  62. package/.wrangler/tmp/bundle-Z9Ytbr/checked-fetch.js +0 -30
  63. package/.wrangler/tmp/bundle-Z9Ytbr/middleware-insertion-facade.js +0 -11
  64. package/.wrangler/tmp/bundle-Z9Ytbr/middleware-loader.entry.ts +0 -134
  65. package/.wrangler/tmp/bundle-agEOGG/checked-fetch.js +0 -30
  66. package/.wrangler/tmp/bundle-agEOGG/middleware-insertion-facade.js +0 -11
  67. package/.wrangler/tmp/bundle-agEOGG/middleware-loader.entry.ts +0 -134
  68. package/.wrangler/tmp/bundle-bKzVij/checked-fetch.js +0 -30
  69. package/.wrangler/tmp/bundle-bKzVij/middleware-insertion-facade.js +0 -11
  70. package/.wrangler/tmp/bundle-bKzVij/middleware-loader.entry.ts +0 -134
  71. package/.wrangler/tmp/bundle-ehd4TO/checked-fetch.js +0 -30
  72. package/.wrangler/tmp/bundle-ehd4TO/middleware-insertion-facade.js +0 -11
  73. package/.wrangler/tmp/bundle-ehd4TO/middleware-loader.entry.ts +0 -134
  74. package/.wrangler/tmp/bundle-fU4auK/checked-fetch.js +0 -30
  75. package/.wrangler/tmp/bundle-fU4auK/middleware-insertion-facade.js +0 -11
  76. package/.wrangler/tmp/bundle-fU4auK/middleware-loader.entry.ts +0 -134
  77. package/.wrangler/tmp/bundle-hcIiZ8/checked-fetch.js +0 -30
  78. package/.wrangler/tmp/bundle-hcIiZ8/middleware-insertion-facade.js +0 -11
  79. package/.wrangler/tmp/bundle-hcIiZ8/middleware-loader.entry.ts +0 -134
  80. package/.wrangler/tmp/bundle-iLarYT/checked-fetch.js +0 -30
  81. package/.wrangler/tmp/bundle-iLarYT/middleware-insertion-facade.js +0 -11
  82. package/.wrangler/tmp/bundle-iLarYT/middleware-loader.entry.ts +0 -134
  83. package/.wrangler/tmp/bundle-kh10f5/checked-fetch.js +0 -30
  84. package/.wrangler/tmp/bundle-kh10f5/middleware-insertion-facade.js +0 -11
  85. package/.wrangler/tmp/bundle-kh10f5/middleware-loader.entry.ts +0 -134
  86. package/.wrangler/tmp/bundle-kh10f5/strip-cf-connecting-ip-header.js +0 -13
  87. package/.wrangler/tmp/bundle-kxse6R/checked-fetch.js +0 -30
  88. package/.wrangler/tmp/bundle-kxse6R/middleware-insertion-facade.js +0 -11
  89. package/.wrangler/tmp/bundle-kxse6R/middleware-loader.entry.ts +0 -134
  90. package/.wrangler/tmp/bundle-lMGAOP/checked-fetch.js +0 -30
  91. package/.wrangler/tmp/bundle-lMGAOP/middleware-insertion-facade.js +0 -11
  92. package/.wrangler/tmp/bundle-lMGAOP/middleware-loader.entry.ts +0 -134
  93. package/.wrangler/tmp/bundle-mVsIPC/checked-fetch.js +0 -30
  94. package/.wrangler/tmp/bundle-mVsIPC/middleware-insertion-facade.js +0 -11
  95. package/.wrangler/tmp/bundle-mVsIPC/middleware-loader.entry.ts +0 -134
  96. package/.wrangler/tmp/bundle-mlXKeI/checked-fetch.js +0 -30
  97. package/.wrangler/tmp/bundle-mlXKeI/middleware-insertion-facade.js +0 -11
  98. package/.wrangler/tmp/bundle-mlXKeI/middleware-loader.entry.ts +0 -134
  99. package/.wrangler/tmp/bundle-oqR7ot/checked-fetch.js +0 -30
  100. package/.wrangler/tmp/bundle-oqR7ot/middleware-insertion-facade.js +0 -11
  101. package/.wrangler/tmp/bundle-oqR7ot/middleware-loader.entry.ts +0 -134
  102. package/.wrangler/tmp/bundle-r6Dv1e/checked-fetch.js +0 -30
  103. package/.wrangler/tmp/bundle-r6Dv1e/middleware-insertion-facade.js +0 -11
  104. package/.wrangler/tmp/bundle-r6Dv1e/middleware-loader.entry.ts +0 -134
  105. package/.wrangler/tmp/bundle-wLgVka/checked-fetch.js +0 -30
  106. package/.wrangler/tmp/bundle-wLgVka/middleware-insertion-facade.js +0 -11
  107. package/.wrangler/tmp/bundle-wLgVka/middleware-loader.entry.ts +0 -134
  108. package/.wrangler/tmp/bundle-xVaLLy/checked-fetch.js +0 -30
  109. package/.wrangler/tmp/bundle-xVaLLy/middleware-insertion-facade.js +0 -11
  110. package/.wrangler/tmp/bundle-xVaLLy/middleware-loader.entry.ts +0 -134
  111. package/.wrangler/tmp/dev-0nPmAG/worker.js +0 -6194
  112. package/.wrangler/tmp/dev-0nPmAG/worker.js.map +0 -8
  113. package/.wrangler/tmp/dev-1aTYmG/worker.js +0 -6611
  114. package/.wrangler/tmp/dev-1aTYmG/worker.js.map +0 -8
  115. package/.wrangler/tmp/dev-1osU6W/worker.js +0 -6610
  116. package/.wrangler/tmp/dev-1osU6W/worker.js.map +0 -8
  117. package/.wrangler/tmp/dev-21Njjx/worker.js +0 -6611
  118. package/.wrangler/tmp/dev-21Njjx/worker.js.map +0 -8
  119. package/.wrangler/tmp/dev-26GSGQ/worker.js +0 -6253
  120. package/.wrangler/tmp/dev-26GSGQ/worker.js.map +0 -8
  121. package/.wrangler/tmp/dev-31oRGo/worker.js +0 -6194
  122. package/.wrangler/tmp/dev-31oRGo/worker.js.map +0 -8
  123. package/.wrangler/tmp/dev-3PUe1f/worker.js +0 -6194
  124. package/.wrangler/tmp/dev-3PUe1f/worker.js.map +0 -8
  125. package/.wrangler/tmp/dev-49noGf/worker.js +0 -6268
  126. package/.wrangler/tmp/dev-49noGf/worker.js.map +0 -8
  127. package/.wrangler/tmp/dev-4IHUrW/worker.js +0 -6194
  128. package/.wrangler/tmp/dev-4IHUrW/worker.js.map +0 -8
  129. package/.wrangler/tmp/dev-A9zJjT/worker.js +0 -6194
  130. package/.wrangler/tmp/dev-A9zJjT/worker.js.map +0 -8
  131. package/.wrangler/tmp/dev-BRrYv0/worker.js +0 -6512
  132. package/.wrangler/tmp/dev-BRrYv0/worker.js.map +0 -8
  133. package/.wrangler/tmp/dev-IAjPvs/worker.js +0 -6194
  134. package/.wrangler/tmp/dev-IAjPvs/worker.js.map +0 -8
  135. package/.wrangler/tmp/dev-NlAyrN/worker.js +0 -6512
  136. package/.wrangler/tmp/dev-NlAyrN/worker.js.map +0 -8
  137. package/.wrangler/tmp/dev-RAb75q/worker.js +0 -6194
  138. package/.wrangler/tmp/dev-RAb75q/worker.js.map +0 -8
  139. package/.wrangler/tmp/dev-SzA1fi/worker.js +0 -6610
  140. package/.wrangler/tmp/dev-SzA1fi/worker.js.map +0 -8
  141. package/.wrangler/tmp/dev-Z6uo4O/worker.js +0 -6194
  142. package/.wrangler/tmp/dev-Z6uo4O/worker.js.map +0 -8
  143. package/.wrangler/tmp/dev-Zv3JSE/worker.js +0 -6094
  144. package/.wrangler/tmp/dev-Zv3JSE/worker.js.map +0 -8
  145. package/.wrangler/tmp/dev-aPPB6u/worker.js +0 -6498
  146. package/.wrangler/tmp/dev-aPPB6u/worker.js.map +0 -8
  147. package/.wrangler/tmp/dev-b6RHui/worker.js +0 -6253
  148. package/.wrangler/tmp/dev-b6RHui/worker.js.map +0 -8
  149. package/.wrangler/tmp/dev-eD5RAO/worker.js +0 -6284
  150. package/.wrangler/tmp/dev-eD5RAO/worker.js.map +0 -8
  151. package/.wrangler/tmp/dev-fvSQn5/worker.js +0 -6099
  152. package/.wrangler/tmp/dev-fvSQn5/worker.js.map +0 -8
  153. package/.wrangler/tmp/dev-h1SJ6S/worker.js +0 -6253
  154. package/.wrangler/tmp/dev-h1SJ6S/worker.js.map +0 -8
  155. package/.wrangler/tmp/dev-ivThQ6/worker.js +0 -6268
  156. package/.wrangler/tmp/dev-ivThQ6/worker.js.map +0 -8
  157. package/.wrangler/tmp/dev-iwU6dw/worker.js +0 -6611
  158. package/.wrangler/tmp/dev-iwU6dw/worker.js.map +0 -8
  159. package/.wrangler/tmp/dev-iwvsJt/worker.js +0 -6284
  160. package/.wrangler/tmp/dev-iwvsJt/worker.js.map +0 -8
  161. package/.wrangler/tmp/dev-kYVP9x/worker.js +0 -6268
  162. package/.wrangler/tmp/dev-kYVP9x/worker.js.map +0 -8
  163. package/.wrangler/tmp/dev-l39YAw/worker.js +0 -6194
  164. package/.wrangler/tmp/dev-l39YAw/worker.js.map +0 -8
  165. package/.wrangler/tmp/dev-vT7ODU/worker.js +0 -6253
  166. package/.wrangler/tmp/dev-vT7ODU/worker.js.map +0 -8
  167. package/.wrangler/tmp/dev-w6wgcm/worker.js +0 -6611
  168. package/.wrangler/tmp/dev-w6wgcm/worker.js.map +0 -8
  169. package/.wrangler/tmp/dev-yxONWu/worker.js +0 -6194
  170. package/.wrangler/tmp/dev-yxONWu/worker.js.map +0 -8
package/README.md CHANGED
@@ -60,33 +60,43 @@ await client.run(sql`SELECT * FROM users`);
60
60
  ```
61
61
 
62
62
  ### Transaction Compatibility
63
- **Important**: Cloudflare D1 does not support real SQL transactions. This adapter provides a compatibility layer that allows transaction-based code to run without modifications, but **without transaction guarantees**:
63
+ **Breaking in 2.0.0**: Cloudflare D1 has no interactive transactions. `transaction()` now throws `D1TransactionUnsupportedError` instead of running the callback without atomicity. The callback is never invoked.
64
64
 
65
65
  ```typescript
66
- // This code will run without errors, but operations are NOT atomic
67
- await client.transaction(async (tx) => {
68
- await tx.insert(users).values({ name: 'John' });
69
- await tx.update(posts).set({ published: 1 });
70
- // If an error occurs here, previous operations will NOT be rolled back
71
- });
66
+ import { D1TransactionUnsupportedError } from '@drizzle-adapter/d1';
67
+
68
+ try {
69
+ await client.transaction(async (tx) => { /* never runs */ });
70
+ } catch (caughtError) {
71
+ if (caughtError instanceof D1TransactionUnsupportedError) {
72
+ // use db.batch() instead
73
+ }
74
+ }
72
75
  ```
73
76
 
74
- **For atomic operations**, use D1's batch API directly:
77
+ **For atomic operations**, use `db.batch()`. It runs as one implicit SQL transaction (all-or-nothing):
75
78
 
76
79
  ```typescript
77
- // This provides atomic execution - all succeed or all fail
80
+ import { sql } from 'drizzle-orm';
81
+
78
82
  const results = await client.batch([
79
- client.insert(users).values({ name: 'John' }).prepare(),
80
- client.update(posts).set({ published: 1 }).prepare()
83
+ client.insert(users).values({ name: 'John' }),
84
+ client.run(sql`UPDATE posts SET published = 1 WHERE id = ${postId}`)
81
85
  ]);
82
86
  ```
83
87
 
84
88
  ### Migration Considerations
85
- - **Transaction Compatibility**: Existing code using `db.transaction()` will work without modification
89
+ - **Transaction Compatibility**: Since 2.0.0, `db.transaction()` throws `D1TransactionUnsupportedError`; migrate to `db.batch()`
86
90
  - **No Rollback**: Failed operations cannot be automatically rolled back
87
91
  - **Batch Operations**: Use `db.batch()` for operations that require atomicity
88
92
  - **Error Handling**: Implement manual cleanup logic for critical operations
89
93
 
94
+ ### Releasing
95
+ ```bash
96
+ pnpm check && pnpm lint && pnpm test
97
+ pnpm --filter @drizzle-adapter/d1 publish --access public # run from the repo root
98
+ ```
99
+
90
100
  ## Usage
91
101
 
92
102
  ### Cloudflare Workers Setup
@@ -1,4 +1,5 @@
1
1
  import { drizzle } from 'drizzle-orm/d1';
2
+ import { D1TransactionUnsupportedError } from '../Error/D1TransactionUnsupportedError.js';
2
3
  /**
3
4
  * D1 connection implementation for Drizzle ORM.
4
5
  * Manages database connections and provides client access for D1 databases.
@@ -15,14 +16,16 @@ export class D1DrizzleConnection {
15
16
  const database = drizzle(driver);
16
17
  database.execute =
17
18
  database.run;
18
- // Override transaction method to provide transaction-like behavior
19
- // Since D1 doesn't support real transactions, we pass the original client
20
- // Users can implement their own batching if needed for atomicity
21
- database.transaction = async (callback) => {
22
- // For now, just pass the original client - no transaction isolation
23
- // This allows code to work without modifications while being explicit about the limitation
24
- return await callback(database);
25
- };
19
+ /**
20
+ * Replaces drizzle's transaction. drizzle-orm d1/session.js transaction() issues raw `begin`, which D1 rejects (probe in 1TS-Monorepo#4023).
21
+ * The callback is never run; callers must use batch().
22
+ * @returns {Promise<never>} Always rejects with D1TransactionUnsupportedError.
23
+ */
24
+ database.transaction =
25
+ async () => {
26
+ // `return await` satisfies both require-await and the no-bare-throw lint rules
27
+ return await Promise.reject(new D1TransactionUnsupportedError());
28
+ };
26
29
  this.client = database;
27
30
  }
28
31
  /**
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Thrown by transaction() on a D1 client. D1 has no interactive transactions,
3
+ * so the callback could not run atomically.
4
+ */
5
+ export declare class D1TransactionUnsupportedError extends Error {
6
+ /**
7
+ * Constructor.
8
+ */
9
+ constructor();
10
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Thrown by transaction() on a D1 client. D1 has no interactive transactions,
3
+ * so the callback could not run atomically.
4
+ */
5
+ export class D1TransactionUnsupportedError extends Error {
6
+ /**
7
+ * Constructor.
8
+ */
9
+ constructor() {
10
+ super('D1 has no interactive transactions, so transaction() would run your callback without atomicity. Use db.batch([...]) instead: it is atomic (one implicit SQL transaction, all-or-nothing). Wrap raw SQL as db.run(sql`...`) inside the batch.');
11
+ this.name = 'D1TransactionUnsupportedError';
12
+ }
13
+ }
@@ -13,5 +13,6 @@ export interface D1DrizzleClientInterface extends DrizzleClientInterface {
13
13
  selectDistinct(...args: any[]): ReturnType<AnyD1Database['selectDistinct']>;
14
14
  update(...args: any[]): ReturnType<AnyD1Database['update']>;
15
15
  insert(...args: any[]): ReturnType<AnyD1Database['insert']>;
16
- transaction(...args: any[]): ReturnType<AnyD1Database['transaction']>;
16
+ /** Always rejects with D1TransactionUnsupportedError — use batch(). */
17
+ transaction(...args: unknown[]): Promise<never>;
17
18
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { D1DrizzleAdapterFactory } from './Factory/D1DrizzleAdapterFactory.js';
2
2
  export { D1DrizzleAdapter } from './Adapter/D1DrizzleAdapter.js';
3
3
  export { D1DrizzleConnection } from './Adapter/D1DrizzleConnection.js';
4
+ export { D1TransactionUnsupportedError } from './Error/D1TransactionUnsupportedError.js';
4
5
  export { D1DrizzleAdapterFactory } from './Factory/D1DrizzleAdapterFactory.js';
5
6
  export { D1DrizzleDataTypesFactory } from './Factory/D1DrizzleDataTypesFactory.js';
6
7
  export * from './Interface/D1DrizzleClientInterface.js';
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { D1DrizzleAdapterFactory } from './Factory/D1DrizzleAdapterFactory.js';
4
4
  import { D1DrizzleDataTypesFactory } from './Factory/D1DrizzleDataTypesFactory.js';
5
5
  export { D1DrizzleAdapter } from './Adapter/D1DrizzleAdapter.js';
6
6
  export { D1DrizzleConnection } from './Adapter/D1DrizzleConnection.js';
7
+ export { D1TransactionUnsupportedError } from './Error/D1TransactionUnsupportedError.js';
7
8
  export { D1DrizzleAdapterFactory } from './Factory/D1DrizzleAdapterFactory.js';
8
9
  export { D1DrizzleDataTypesFactory } from './Factory/D1DrizzleDataTypesFactory.js';
9
10
  export * from './Interface/D1DrizzleClientInterface.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@drizzle-adapter/d1",
3
3
  "type": "module",
4
- "version": "1.1.1",
4
+ "version": "2.0.1",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
@@ -20,6 +20,7 @@
20
20
  "dotenv": "^17.3.1",
21
21
  "eslint": "^10.1.0",
22
22
  "eslint-plugin-unused-imports": "^4.4.1",
23
+ "miniflare": "4.20260317.2",
23
24
  "shx": "^0.4.0",
24
25
  "tsc-esm-fix": "^3.1.2",
25
26
  "tsconfig-paths": "^4.2.0",
@@ -2,6 +2,7 @@ import type { D1Database } from '@cloudflare/workers-types';
2
2
  import type { DrizzleConnectionInterface } from '@drizzle-adapter/core';
3
3
  import { drizzle } from 'drizzle-orm/d1';
4
4
 
5
+ import { D1TransactionUnsupportedError } from '../Error/D1TransactionUnsupportedError';
5
6
  import type { D1DrizzleClientInterface } from '../Interface/D1DrizzleClientInterface';
6
7
 
7
8
  /**
@@ -26,21 +27,19 @@ export class D1DrizzleConnection implements DrizzleConnectionInterface
26
27
  (database as unknown as D1DrizzleClientInterface).execute =
27
28
  database.run;
28
29
 
29
- // Override transaction method to provide transaction-like behavior
30
- // Since D1 doesn't support real transactions, we pass the original client
31
- // Users can implement their own batching if needed for atomicity
32
- (database as unknown as D1DrizzleClientInterface).transaction = async <
33
- T
34
- >(
35
- callback: (tx: D1DrizzleClientInterface) => Promise<T>
36
- ): Promise<T> =>
37
- {
38
- // For now, just pass the original client - no transaction isolation
39
- // This allows code to work without modifications while being explicit about the limitation
40
- return await callback(
41
- database as unknown as D1DrizzleClientInterface
42
- );
43
- };
30
+ /**
31
+ * Replaces drizzle's transaction. drizzle-orm d1/session.js transaction() issues raw `begin`, which D1 rejects (probe in 1TS-Monorepo#4023).
32
+ * The callback is never run; callers must use batch().
33
+ * @returns {Promise<never>} Always rejects with D1TransactionUnsupportedError.
34
+ */
35
+ (database as unknown as D1DrizzleClientInterface).transaction =
36
+ async (): Promise<never> =>
37
+ {
38
+ // `return await` satisfies both require-await and the no-bare-throw lint rules
39
+ return await Promise.reject(
40
+ new D1TransactionUnsupportedError()
41
+ );
42
+ };
44
43
 
45
44
  this.client = database as unknown as D1DrizzleClientInterface;
46
45
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Thrown by transaction() on a D1 client. D1 has no interactive transactions,
3
+ * so the callback could not run atomically.
4
+ */
5
+ export class D1TransactionUnsupportedError extends Error
6
+ {
7
+ /**
8
+ * Constructor.
9
+ */
10
+ constructor()
11
+ {
12
+ super(
13
+ 'D1 has no interactive transactions, so transaction() would run your callback without atomicity. Use db.batch([...]) instead: it is atomic (one implicit SQL transaction, all-or-nothing). Wrap raw SQL as db.run(sql`...`) inside the batch.'
14
+ );
15
+ this.name = 'D1TransactionUnsupportedError';
16
+ }
17
+ }
@@ -15,5 +15,6 @@ export interface D1DrizzleClientInterface extends DrizzleClientInterface
15
15
  selectDistinct(...args: any[]): ReturnType<AnyD1Database['selectDistinct']>;
16
16
  update(...args: any[]): ReturnType<AnyD1Database['update']>;
17
17
  insert(...args: any[]): ReturnType<AnyD1Database['insert']>;
18
- transaction(...args: any[]): ReturnType<AnyD1Database['transaction']>;
18
+ /** Always rejects with D1TransactionUnsupportedError — use batch(). */
19
+ transaction(...args: unknown[]): Promise<never>;
19
20
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { D1DrizzleDataTypesFactory } from './Factory/D1DrizzleDataTypesFactory';
9
9
 
10
10
  export { D1DrizzleAdapter } from './Adapter/D1DrizzleAdapter';
11
11
  export { D1DrizzleConnection } from './Adapter/D1DrizzleConnection';
12
+ export { D1TransactionUnsupportedError } from './Error/D1TransactionUnsupportedError';
12
13
  export { D1DrizzleAdapterFactory } from './Factory/D1DrizzleAdapterFactory';
13
14
  export { D1DrizzleDataTypesFactory } from './Factory/D1DrizzleDataTypesFactory';
14
15
  export * from './Interface/D1DrizzleClientInterface';
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
2
2
  import { D1DrizzleConnection } from '../../src/Adapter/D1DrizzleConnection';
3
3
  import { drizzle } from 'drizzle-orm/d1';
4
4
  import type { D1Database } from '@cloudflare/workers-types';
5
+ import { D1TransactionUnsupportedError } from '../../src/Error/D1TransactionUnsupportedError';
5
6
 
6
7
  // Mock drizzle-orm/d1
7
8
  vi.mock('drizzle-orm/d1', () => ({
@@ -37,4 +38,13 @@ describe('D1DrizzleConnection', () => {
37
38
  const client = connection.getClient();
38
39
  expect(client).toMatchObject({ mockedClient: true });
39
40
  });
41
+
42
+ it('transaction() rejects with D1TransactionUnsupportedError and never runs the callback', async () => {
43
+ const connection = new D1DrizzleConnection(mockD1Database);
44
+ const callback = vi.fn();
45
+ const transactionPromise = (connection.getClient() as unknown as { transaction: (cb: typeof callback) => Promise<unknown> }).transaction(callback);
46
+ await expect(transactionPromise).rejects.toBeInstanceOf(D1TransactionUnsupportedError);
47
+ await expect(transactionPromise).rejects.toThrow('db.batch');
48
+ expect(callback).not.toHaveBeenCalled();
49
+ });
40
50
  });
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as packageEntry from '../../src/index';
3
+
4
+ describe('D1TransactionUnsupportedError', () => {
5
+ it('is exported from the package entry point', () => {
6
+ expect(packageEntry.D1TransactionUnsupportedError).toBeDefined();
7
+ const error = new packageEntry.D1TransactionUnsupportedError();
8
+ expect(error).toBeInstanceOf(Error);
9
+ expect(error.name).toBe('D1TransactionUnsupportedError');
10
+ expect(error.message).toContain('db.batch');
11
+ });
12
+ });
@@ -0,0 +1,77 @@
1
+ import type { D1Database } from '@cloudflare/workers-types';
2
+ import { sql } from 'drizzle-orm';
3
+ import type { DrizzleD1Database } from 'drizzle-orm/d1';
4
+ import { Miniflare } from 'miniflare';
5
+ import type { D1Database as MiniflareD1Database } from 'miniflare';
6
+ import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
7
+ import { D1DrizzleConnection } from '../../src/Adapter/D1DrizzleConnection';
8
+ import { D1TransactionUnsupportedError } from '../../src/Error/D1TransactionUnsupportedError';
9
+
10
+ describe('D1 atomicity (real Miniflare D1)', () => {
11
+ let miniflare: Miniflare;
12
+ let miniflareDatabase: MiniflareD1Database;
13
+
14
+ beforeAll(async () => {
15
+ miniflare = new Miniflare({
16
+ modules: true,
17
+ script: 'export default {fetch(){return new Response("")}}',
18
+ d1Databases: ['DB']
19
+ });
20
+ miniflareDatabase = await miniflare.getD1Database('DB');
21
+ await miniflareDatabase.exec('CREATE TABLE probe (id INTEGER PRIMARY KEY, name TEXT UNIQUE)');
22
+ });
23
+
24
+ afterAll(async () => {
25
+ await miniflare.dispose();
26
+ });
27
+
28
+ // Real adapter client. batch() and run() exist on the drizzle D1 client at runtime
29
+ // but are not on D1DrizzleClientInterface, so the client is cast to DrizzleD1Database.
30
+ const buildAdapterClient = (): DrizzleD1Database =>
31
+ new D1DrizzleConnection(miniflareDatabase as unknown as D1Database).getClient() as unknown as DrizzleD1Database;
32
+
33
+ const countProbeRows = async (): Promise<number> => {
34
+ const result = await miniflareDatabase
35
+ .prepare('SELECT count(*) AS total FROM probe')
36
+ .first<{ total: number }>();
37
+ return Number(result?.total);
38
+ };
39
+
40
+ const clearProbeRows = async (): Promise<void> => {
41
+ await miniflareDatabase.exec('DELETE FROM probe');
42
+ };
43
+
44
+ it('transaction() rejects with D1TransactionUnsupportedError and never runs the callback', async () => {
45
+ await clearProbeRows();
46
+ const client = buildAdapterClient();
47
+ const callback = vi.fn(async () => 'ran');
48
+ await expect(
49
+ (client as unknown as { transaction: (work: typeof callback) => Promise<unknown> }).transaction(callback)
50
+ ).rejects.toBeInstanceOf(D1TransactionUnsupportedError);
51
+ expect(callback).not.toHaveBeenCalled();
52
+ expect(await countProbeRows()).toBe(0);
53
+ });
54
+
55
+ // drizzle-orm d1/session.js batch() forwards to native D1Database.batch, which is one implicit SQL transaction.
56
+ it('a batch with a UNIQUE violation is all-or-nothing', async () => {
57
+ await clearProbeRows();
58
+ const client = buildAdapterClient();
59
+ await expect(
60
+ client.batch([
61
+ client.run(sql`INSERT INTO probe (name) VALUES ('dup')`),
62
+ client.run(sql`INSERT INTO probe (name) VALUES ('dup')`)
63
+ ])
64
+ ).rejects.toThrow(/UNIQUE/);
65
+ expect(await countProbeRows()).toBe(0);
66
+ });
67
+
68
+ it('a batch of two distinct inserts commits both rows (control)', async () => {
69
+ await clearProbeRows();
70
+ const client = buildAdapterClient();
71
+ await client.batch([
72
+ client.run(sql`INSERT INTO probe (name) VALUES ('alpha')`),
73
+ client.run(sql`INSERT INTO probe (name) VALUES ('beta')`)
74
+ ]);
75
+ expect(await countProbeRows()).toBe(2);
76
+ });
77
+ });
@@ -1,30 +0,0 @@
1
- const urls = new Set();
2
-
3
- function checkURL(request, init) {
4
- const url =
5
- request instanceof URL
6
- ? request
7
- : new URL(
8
- (typeof request === "string"
9
- ? new Request(request, init)
10
- : request
11
- ).url
12
- );
13
- if (url.port && url.port !== "443" && url.protocol === "https:") {
14
- if (!urls.has(url.toString())) {
15
- urls.add(url.toString());
16
- console.warn(
17
- `WARNING: known issue with \`fetch()\` requests to custom HTTPS ports in published Workers:\n` +
18
- ` - ${url.toString()} - the custom port will be ignored when the Worker is published using the \`wrangler deploy\` command.\n`
19
- );
20
- }
21
- }
22
- }
23
-
24
- globalThis.fetch = new Proxy(globalThis.fetch, {
25
- apply(target, thisArg, argArray) {
26
- const [request, init] = argArray;
27
- checkURL(request, init);
28
- return Reflect.apply(target, thisArg, argArray);
29
- },
30
- });
@@ -1,11 +0,0 @@
1
- import worker, * as OTHER_EXPORTS from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/tests/worker.ts";
2
- import * as __MIDDLEWARE_0__ from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/node_modules/.pnpm/wrangler@3.91.0_@cloudflare+workers-types@4.20241127.0/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts";
3
- import * as __MIDDLEWARE_1__ from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/node_modules/.pnpm/wrangler@3.91.0_@cloudflare+workers-types@4.20241127.0/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts";
4
-
5
- export * from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/tests/worker.ts";
6
-
7
- export const __INTERNAL_WRANGLER_MIDDLEWARE__ = [
8
-
9
- __MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default
10
- ]
11
- export default worker;
@@ -1,134 +0,0 @@
1
- // This loads all middlewares exposed on the middleware object and then starts
2
- // the invocation chain. The big idea is that we can add these to the middleware
3
- // export dynamically through wrangler, or we can potentially let users directly
4
- // add them as a sort of "plugin" system.
5
-
6
- import ENTRY, { __INTERNAL_WRANGLER_MIDDLEWARE__ } from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/.wrangler/tmp/bundle-5POwQi/middleware-insertion-facade.js";
7
- import { __facade_invoke__, __facade_register__, Dispatcher } from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/node_modules/.pnpm/wrangler@3.91.0_@cloudflare+workers-types@4.20241127.0/node_modules/wrangler/templates/middleware/common.ts";
8
- import type { WorkerEntrypointConstructor } from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/.wrangler/tmp/bundle-5POwQi/middleware-insertion-facade.js";
9
-
10
- // Preserve all the exports from the worker
11
- export * from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/.wrangler/tmp/bundle-5POwQi/middleware-insertion-facade.js";
12
-
13
- class __Facade_ScheduledController__ implements ScheduledController {
14
- readonly #noRetry: ScheduledController["noRetry"];
15
-
16
- constructor(
17
- readonly scheduledTime: number,
18
- readonly cron: string,
19
- noRetry: ScheduledController["noRetry"]
20
- ) {
21
- this.#noRetry = noRetry;
22
- }
23
-
24
- noRetry() {
25
- if (!(this instanceof __Facade_ScheduledController__)) {
26
- throw new TypeError("Illegal invocation");
27
- }
28
- // Need to call native method immediately in case uncaught error thrown
29
- this.#noRetry();
30
- }
31
- }
32
-
33
- function wrapExportedHandler(worker: ExportedHandler): ExportedHandler {
34
- // If we don't have any middleware defined, just return the handler as is
35
- if (
36
- __INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
37
- __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
38
- ) {
39
- return worker;
40
- }
41
- // Otherwise, register all middleware once
42
- for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
43
- __facade_register__(middleware);
44
- }
45
-
46
- const fetchDispatcher: ExportedHandlerFetchHandler = function (
47
- request,
48
- env,
49
- ctx
50
- ) {
51
- if (worker.fetch === undefined) {
52
- throw new Error("Handler does not export a fetch() function.");
53
- }
54
- return worker.fetch(request, env, ctx);
55
- };
56
-
57
- return {
58
- ...worker,
59
- fetch(request, env, ctx) {
60
- const dispatcher: Dispatcher = function (type, init) {
61
- if (type === "scheduled" && worker.scheduled !== undefined) {
62
- const controller = new __Facade_ScheduledController__(
63
- Date.now(),
64
- init.cron ?? "",
65
- () => {}
66
- );
67
- return worker.scheduled(controller, env, ctx);
68
- }
69
- };
70
- return __facade_invoke__(request, env, ctx, dispatcher, fetchDispatcher);
71
- },
72
- };
73
- }
74
-
75
- function wrapWorkerEntrypoint(
76
- klass: WorkerEntrypointConstructor
77
- ): WorkerEntrypointConstructor {
78
- // If we don't have any middleware defined, just return the handler as is
79
- if (
80
- __INTERNAL_WRANGLER_MIDDLEWARE__ === undefined ||
81
- __INTERNAL_WRANGLER_MIDDLEWARE__.length === 0
82
- ) {
83
- return klass;
84
- }
85
- // Otherwise, register all middleware once
86
- for (const middleware of __INTERNAL_WRANGLER_MIDDLEWARE__) {
87
- __facade_register__(middleware);
88
- }
89
-
90
- // `extend`ing `klass` here so other RPC methods remain callable
91
- return class extends klass {
92
- #fetchDispatcher: ExportedHandlerFetchHandler<Record<string, unknown>> = (
93
- request,
94
- env,
95
- ctx
96
- ) => {
97
- this.env = env;
98
- this.ctx = ctx;
99
- if (super.fetch === undefined) {
100
- throw new Error("Entrypoint class does not define a fetch() function.");
101
- }
102
- return super.fetch(request);
103
- };
104
-
105
- #dispatcher: Dispatcher = (type, init) => {
106
- if (type === "scheduled" && super.scheduled !== undefined) {
107
- const controller = new __Facade_ScheduledController__(
108
- Date.now(),
109
- init.cron ?? "",
110
- () => {}
111
- );
112
- return super.scheduled(controller);
113
- }
114
- };
115
-
116
- fetch(request: Request<unknown, IncomingRequestCfProperties>) {
117
- return __facade_invoke__(
118
- request,
119
- this.env,
120
- this.ctx,
121
- this.#dispatcher,
122
- this.#fetchDispatcher
123
- );
124
- }
125
- };
126
- }
127
-
128
- let WRAPPED_ENTRY: ExportedHandler | WorkerEntrypointConstructor | undefined;
129
- if (typeof ENTRY === "object") {
130
- WRAPPED_ENTRY = wrapExportedHandler(ENTRY);
131
- } else if (typeof ENTRY === "function") {
132
- WRAPPED_ENTRY = wrapWorkerEntrypoint(ENTRY);
133
- }
134
- export default WRAPPED_ENTRY;
@@ -1,30 +0,0 @@
1
- const urls = new Set();
2
-
3
- function checkURL(request, init) {
4
- const url =
5
- request instanceof URL
6
- ? request
7
- : new URL(
8
- (typeof request === "string"
9
- ? new Request(request, init)
10
- : request
11
- ).url
12
- );
13
- if (url.port && url.port !== "443" && url.protocol === "https:") {
14
- if (!urls.has(url.toString())) {
15
- urls.add(url.toString());
16
- console.warn(
17
- `WARNING: known issue with \`fetch()\` requests to custom HTTPS ports in published Workers:\n` +
18
- ` - ${url.toString()} - the custom port will be ignored when the Worker is published using the \`wrangler deploy\` command.\n`
19
- );
20
- }
21
- }
22
- }
23
-
24
- globalThis.fetch = new Proxy(globalThis.fetch, {
25
- apply(target, thisArg, argArray) {
26
- const [request, init] = argArray;
27
- checkURL(request, init);
28
- return Reflect.apply(target, thisArg, argArray);
29
- },
30
- });
@@ -1,11 +0,0 @@
1
- import worker, * as OTHER_EXPORTS from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/tests/worker.ts";
2
- import * as __MIDDLEWARE_0__ from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/node_modules/.pnpm/wrangler@3.91.0_@cloudflare+workers-types@4.20241127.0/node_modules/wrangler/templates/middleware/middleware-ensure-req-body-drained.ts";
3
- import * as __MIDDLEWARE_1__ from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/node_modules/.pnpm/wrangler@3.91.0_@cloudflare+workers-types@4.20241127.0/node_modules/wrangler/templates/middleware/middleware-miniflare3-json-error.ts";
4
-
5
- export * from "/Users/dmitry/Projects/OneTeamSoftware/TypeScript/packages/DrizzleAdapter/packages/d1/tests/worker.ts";
6
-
7
- export const __INTERNAL_WRANGLER_MIDDLEWARE__ = [
8
-
9
- __MIDDLEWARE_0__.default,__MIDDLEWARE_1__.default
10
- ]
11
- export default worker;