@drizzle-adapter/d1 1.1.1 → 2.0.0
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.
- package/README.md +22 -12
- package/dist/Adapter/D1DrizzleConnection.js +11 -8
- package/dist/Error/D1TransactionUnsupportedError.d.ts +10 -0
- package/dist/Error/D1TransactionUnsupportedError.js +13 -0
- package/dist/Interface/D1DrizzleClientInterface.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +17 -15
- package/src/Adapter/D1DrizzleConnection.ts +14 -15
- package/src/Error/D1TransactionUnsupportedError.ts +17 -0
- package/src/Interface/D1DrizzleClientInterface.ts +2 -1
- package/src/index.ts +1 -0
- package/tests/Adapter/D1DrizzleConnection.test.ts +10 -0
- package/tests/Error/D1TransactionUnsupportedError.test.ts +12 -0
- package/tests/Integration/D1Atomicity.miniflare.test.ts +77 -0
package/README.md
CHANGED
|
@@ -60,33 +60,43 @@ await client.run(sql`SELECT * FROM users`);
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
### Transaction Compatibility
|
|
63
|
-
**
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await
|
|
70
|
-
|
|
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
|
|
77
|
+
**For atomic operations**, use `db.batch()`. It runs as one implicit SQL transaction (all-or-nothing):
|
|
75
78
|
|
|
76
79
|
```typescript
|
|
77
|
-
|
|
80
|
+
import { sql } from 'drizzle-orm';
|
|
81
|
+
|
|
78
82
|
const results = await client.batch([
|
|
79
|
-
client.insert(users).values({ name: 'John' })
|
|
80
|
-
client.
|
|
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**:
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,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
|
-
|
|
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": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -14,12 +14,23 @@
|
|
|
14
14
|
"default": "./dist/migrator.js"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"lint": "eslint src/",
|
|
19
|
+
"format": "eslint --fix src/",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"check": "tsc --noEmit",
|
|
22
|
+
"build": "shx rm -rf dist && tsc && tsc-esm-fix",
|
|
23
|
+
"package:tgz": "pnpm pack",
|
|
24
|
+
"package:publish": "pnpm publish --access public",
|
|
25
|
+
"prepublishOnly": "pnpm run build"
|
|
26
|
+
},
|
|
17
27
|
"devDependencies": {
|
|
18
28
|
"@cloudflare/workers-types": "^4.20260317.1",
|
|
19
29
|
"@dmitryrechkin/eslint-standard": "^1.5.13",
|
|
20
30
|
"dotenv": "^17.3.1",
|
|
21
31
|
"eslint": "^10.1.0",
|
|
22
32
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
33
|
+
"miniflare": "4.20260317.2",
|
|
23
34
|
"shx": "^0.4.0",
|
|
24
35
|
"tsc-esm-fix": "^3.1.2",
|
|
25
36
|
"tsconfig-paths": "^4.2.0",
|
|
@@ -28,18 +39,9 @@
|
|
|
28
39
|
"wrangler": "^4.77.0"
|
|
29
40
|
},
|
|
30
41
|
"dependencies": {
|
|
31
|
-
"drizzle-
|
|
32
|
-
"@drizzle-adapter/core": "
|
|
33
|
-
"
|
|
42
|
+
"@drizzle-adapter/core": "workspace:^",
|
|
43
|
+
"@drizzle-adapter/sqlite-core": "workspace:^",
|
|
44
|
+
"drizzle-orm": "^0.45.1"
|
|
34
45
|
},
|
|
35
|
-
"license": "MIT"
|
|
36
|
-
|
|
37
|
-
"lint": "eslint src/",
|
|
38
|
-
"format": "eslint --fix src/",
|
|
39
|
-
"test": "vitest run",
|
|
40
|
-
"check": "tsc --noEmit",
|
|
41
|
-
"build": "shx rm -rf dist && tsc && tsc-esm-fix",
|
|
42
|
-
"package:tgz": "pnpm pack",
|
|
43
|
-
"package:publish": "pnpm publish --access public"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
+
});
|