@gobing-ai/ts-db 0.4.6 → 0.4.8

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 CHANGED
@@ -14,7 +14,7 @@ Application code imports only `@gobing-ai/ts-db` — never `drizzle-orm`. drizzl
14
14
 
15
15
  | Component | Purpose |
16
16
  |-----------|---------|
17
- | `createDbAdapter` / `DbAdapter` | Construction + lifecycle + string-SQL escape; exposes an internal typed db to the DAO layer only |
17
+ | `createDbAdapter` / `DbAdapter` | Construction + lifecycle + string-SQL escape + atomic batch; exposes an internal typed db to the DAO layer only |
18
18
  | `BunSqliteAdapter` | Bun SQLite implementation with statement caching and WAL pragmas (`@gobing-ai/ts-db/bun-sqlite`) |
19
19
  | `D1Adapter` | Cloudflare D1 implementation (no `@cloudflare/workers-types` dependency) |
20
20
  | `BaseDao` | Raw tier — `query`/`one`/`tx`, drizzle-free signatures |
@@ -52,6 +52,7 @@ classDiagram
52
52
  +run(sql, ...params) void
53
53
  +queryFirst(sql, ...params) T?
54
54
  +queryAll(sql, ...params) T[]
55
+ +batch(operations) void
55
56
  +close() void
56
57
  }
57
58
 
@@ -177,6 +178,13 @@ const user = await adapter.queryFirst<{ name: string }>('SELECT name FROM users
177
178
  const all = await adapter.queryAll<{ name: string }>('SELECT name FROM users');
178
179
  ```
179
180
 
181
+ ```ts
182
+ // Atomic multi-statement batch — all succeed or all roll back (ADR-020)
183
+ await adapter.batch([
184
+ { sql: 'INSERT INTO users VALUES (?, ?)', params: ['u2', 'Bob'] },
185
+ { sql: 'UPDATE counters SET count = count + 1 WHERE name = ?', params: ['users'] },
186
+ ]);
187
+ ```
180
188
  ### EntityDao — CRUD with soft delete
181
189
 
182
190
  Define a Drizzle table, extend `EntityDao`, get full CRUD for free:
package/dist/adapter.d.ts CHANGED
@@ -40,9 +40,28 @@ export interface DbAdapter {
40
40
  queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
41
41
  /** Parameterized read returning all rows. */
42
42
  queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
43
+ /**
44
+ * Execute a batch of parameterized writes atomically — either all commit or
45
+ * none do. Used by adapters that need to persist multiple related rows in a
46
+ * single transaction (e.g. workflow transition + state snapshot + phase write).
47
+ * Each entry is a `{ sql, params }` tuple applied in order. Adapters must
48
+ * uphold all-or-nothing semantics for multi-statement batches or **throw**;
49
+ * silently degrading to non-atomic sequential writes is not allowed. A
50
+ * single-statement batch may execute directly (trivially atomic).
51
+ */
52
+ batch(operations: readonly DbBatchOp[]): Promise<void>;
43
53
  /** Close the underlying connection. */
44
54
  close(): void;
45
55
  }
56
+ /**
57
+ * A single write operation inside a {@link DbAdapter.batch} call.
58
+ */
59
+ export interface DbBatchOp {
60
+ /** SQL statement (INSERT/UPDATE/DELETE). */
61
+ sql: string;
62
+ /** Bind parameters for the statement. */
63
+ params: readonly unknown[];
64
+ }
46
65
  /**
47
66
  * Discriminated-union config for creating a database adapter (bun-sqlite or D1).
48
67
  */
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD;;GAEG;AACH,UAAU,SAAS;IACf,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjH;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,wDAAwD;IACxD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,uEAAuE;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,gEAAgE;IAChE,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzE,6CAA6C;IAC7C,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7D,uCAAuC;IACvC,KAAK,IAAI,IAAI,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GACrB;IACI,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACL,GACD;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC;AAE3C;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAcjF"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD;;GAEG;AACH,UAAU,SAAS;IACf,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEjH;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,wDAAwD;IACxD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,uEAAuE;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,gEAAgE;IAChE,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzE,6CAA6C;IAC7C,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAC7D;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,uCAAuC;IACvC,KAAK,IAAI,IAAI,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GACrB;IACI,MAAM,EAAE,YAAY,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACL,GACD;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAC;AAE3C;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAcjF"}
@@ -1,5 +1,5 @@
1
1
  import { type BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
2
- import type { DbAdapter, InternalDb } from '../adapter';
2
+ import type { DbAdapter, DbBatchOp, InternalDb } from '../adapter';
3
3
  import * as schema from '../schema/runtime';
4
4
  /**
5
5
  * Configuration options for the bun:sqlite adapter (path, pragmas).
@@ -36,6 +36,7 @@ export declare class BunSqliteAdapter implements DbAdapter {
36
36
  run(sql: string, ...params: unknown[]): Promise<void>;
37
37
  queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
38
38
  queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
39
+ batch(operations: readonly DbBatchOp[]): Promise<void>;
39
40
  close(): void;
40
41
  }
41
42
  //# sourceMappingURL=bun-sqlite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bun-sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/bun-sqlite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAAW,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAS5C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACL;AAUD;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAC9C,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAmC;IACpD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,OAAO,CAAC,YAAY;gBAUR,OAAO,CAAC,EAAE,gBAAgB;IAkBtC,+EAA+E;IAC/E,IAAI,EAAE,IAAI,UAAU,CAEnB;IAED,wEAAwE;IACxE,YAAY,IAAI,iBAAiB,CAAC,OAAO,MAAM,CAAC;IAI1C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrD,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAKxE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAKlE,KAAK,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"bun-sqlite.d.ts","sourceRoot":"","sources":["../../src/adapters/bun-sqlite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAAW,MAAM,wBAAwB,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAS5C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,OAAO,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACL;AAUD;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAC9C,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,SAAS,CAAmC;IACpD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,OAAO,CAAC,YAAY;gBAUR,OAAO,CAAC,EAAE,gBAAgB;IAkBtC,+EAA+E;IAC/E,IAAI,EAAE,IAAI,UAAU,CAEnB;IAED,wEAAwE;IACxE,YAAY,IAAI,iBAAiB,CAAC,OAAO,MAAM,CAAC;IAI1C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrD,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAKxE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAK5D,KAAK,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5D,KAAK,IAAI,IAAI;CAGhB"}
@@ -65,6 +65,16 @@ export class BunSqliteAdapter {
65
65
  const stmt = this.getStatement(sql);
66
66
  return stmt.all(...params) ?? [];
67
67
  }
68
+ async batch(operations) {
69
+ if (operations.length === 0)
70
+ return;
71
+ this.sqlite.transaction(() => {
72
+ for (const op of operations) {
73
+ const stmt = this.getStatement(op.sql);
74
+ stmt.run(...op.params);
75
+ }
76
+ })();
77
+ }
68
78
  close() {
69
79
  this.sqlite.close();
70
80
  }
@@ -1,5 +1,5 @@
1
1
  import { type DrizzleD1Database } from 'drizzle-orm/d1';
2
- import type { DbAdapter, InternalDb } from '../adapter';
2
+ import type { DbAdapter, DbBatchOp, InternalDb } from '../adapter';
3
3
  import * as schema from '../schema/runtime';
4
4
  /**
5
5
  * Minimal D1 binding interface — avoids depending on @cloudflare/workers-types.
@@ -14,6 +14,14 @@ export interface D1Binding {
14
14
  }>;
15
15
  };
16
16
  exec(sql: string): Promise<void>;
17
+ /**
18
+ * Run multiple statements as a single atomic batch (D1's native `batch()`
19
+ * API). Optional for single-statement test doubles only: when absent,
20
+ * {@link D1Adapter.batch} runs a lone statement directly (trivially atomic)
21
+ * and **throws** for multi-statement batches rather than silently degrading
22
+ * atomicity. Real D1 bindings always provide `batch()`.
23
+ */
24
+ batch?(statements: D1BoundStatement[]): Promise<unknown[]>;
17
25
  }
18
26
  interface D1BoundStatement {
19
27
  all<T>(): Promise<{
@@ -47,6 +55,7 @@ export declare class D1Adapter implements DbAdapter {
47
55
  run(sql: string, ...params: unknown[]): Promise<void>;
48
56
  queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
49
57
  queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
58
+ batch(operations: readonly DbBatchOp[]): Promise<void>;
50
59
  close(): void;
51
60
  }
52
61
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"d1.d.ts","sourceRoot":"","sources":["../../src/adapters/d1.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAW,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAClB,IAAI,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;QAC7C,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/B,GAAG,CAAC,IAAI,OAAO,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAAC,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KAC7D,CAAC;IACF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,UAAU,gBAAgB;IACtB,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtD,GAAG,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzD,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACvB,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,qBAAa,SAAU,YAAW,SAAS;IACvC,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,SAAS,CAAmC;gBAExC,OAAO,EAAE,SAAS;IAK9B,kEAAkE;IAClE,IAAI,EAAE,IAAI,UAAU,CAEnB;IAED,wEAAwE;IACxE,YAAY,IAAI,iBAAiB,CAAC,OAAO,MAAM,CAAC;IAIhD,qEAAqE;IACrE,UAAU,IAAI,SAAS;IAIjB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAQxE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAOlE,KAAK,IAAI,IAAI;CAGhB"}
1
+ {"version":3,"file":"d1.d.ts","sourceRoot":"","sources":["../../src/adapters/d1.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAW,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG;QAClB,IAAI,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC;QAC7C,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAC/B,GAAG,CAAC,IAAI,OAAO,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAAC,OAAO,EAAE,OAAO,CAAA;SAAE,CAAC,CAAC;KAC7D,CAAC;IACF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC;;;;;;OAMG;IACH,KAAK,CAAC,CAAC,UAAU,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CAC9D;AAED,UAAU,gBAAgB;IACtB,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtD,GAAG,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzD,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IACvB,KAAK,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAClC;AAED;;;;;GAKG;AACH,qBAAa,SAAU,YAAW,SAAS;IACvC,OAAO,CAAC,OAAO,CAAY;IAC3B,OAAO,CAAC,SAAS,CAAmC;gBAExC,OAAO,EAAE,SAAS;IAK9B,kEAAkE;IAClE,IAAI,EAAE,IAAI,UAAU,CAEnB;IAED,wEAAwE;IACxE,YAAY,IAAI,iBAAiB,CAAC,OAAO,MAAM,CAAC;IAIhD,qEAAqE;IACrE,UAAU,IAAI,SAAS;IAIjB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAQxE,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAO5D,KAAK,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB5D,KAAK,IAAI,IAAI;CAGhB"}
@@ -44,6 +44,28 @@ export class D1Adapter {
44
44
  const result = await bound.all();
45
45
  return result.results ?? [];
46
46
  }
47
+ async batch(operations) {
48
+ if (operations.length === 0)
49
+ return;
50
+ // A single statement is atomic on its own, so a binding without native
51
+ // batch() may run it directly. Multiple statements without native batch()
52
+ // cannot be made atomic — fail loud rather than silently break the
53
+ // all-or-nothing contract (real D1 bindings always provide batch()).
54
+ if (this.binding.batch === undefined && operations.length > 1) {
55
+ throw new Error(`D1 binding has no batch() — cannot execute ${operations.length} statements atomically; ` +
56
+ 'provide a binding with native batch() support');
57
+ }
58
+ const boundStmts = operations.map((op) => {
59
+ const stmt = this.binding.prepare(op.sql);
60
+ return op.params.length > 0 ? stmt.bind(...op.params) : stmt.bind();
61
+ });
62
+ if (this.binding.batch !== undefined) {
63
+ await this.binding.batch(boundStmts);
64
+ }
65
+ else {
66
+ await boundStmts[0]?.run();
67
+ }
68
+ }
47
69
  close() {
48
70
  // D1 bindings are managed by the Workers runtime -- no-op
49
71
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type InternalDb } from './adapter';
1
+ export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type DbBatchOp, type InternalDb } from './adapter';
2
2
  export { D1Adapter } from './adapters/d1';
3
3
  export { BaseDao, type TxHandle } from './base-dao';
4
4
  export { type EmbeddedMigration, embeddedMigrations } from './embedded-migrations';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACnG,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EACH,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACnH,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,KAAK,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACnF,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACzF,OAAO,EACH,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,cAAc,EACd,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,SAAS,EACd,KAAK,SAAS,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/ts-db",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "@gobing-ai/ts-db — a drizzle-free database facade: typed DAOs over Bun SQLite / Cloudflare D1, a small predicate query spec, single-source-of-truth tables, and migrations. Drizzle stays an internal detail.",
5
5
  "keywords": [
6
6
  "typescript",
@@ -66,8 +66,8 @@
66
66
  "release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-db-v<version> && git push --tags' && exit 1"
67
67
  },
68
68
  "dependencies": {
69
- "@gobing-ai/ts-runtime": "^0.4.6",
70
- "@gobing-ai/ts-utils": "^0.4.6"
69
+ "@gobing-ai/ts-runtime": "^0.4.8",
70
+ "@gobing-ai/ts-utils": "^0.4.8"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "drizzle-orm": ">=0.38.0",
package/src/adapter.ts CHANGED
@@ -43,10 +43,30 @@ export interface DbAdapter {
43
43
  queryFirst<T>(sql: string, ...params: unknown[]): Promise<T | undefined>;
44
44
  /** Parameterized read returning all rows. */
45
45
  queryAll<T>(sql: string, ...params: unknown[]): Promise<T[]>;
46
+ /**
47
+ * Execute a batch of parameterized writes atomically — either all commit or
48
+ * none do. Used by adapters that need to persist multiple related rows in a
49
+ * single transaction (e.g. workflow transition + state snapshot + phase write).
50
+ * Each entry is a `{ sql, params }` tuple applied in order. Adapters must
51
+ * uphold all-or-nothing semantics for multi-statement batches or **throw**;
52
+ * silently degrading to non-atomic sequential writes is not allowed. A
53
+ * single-statement batch may execute directly (trivially atomic).
54
+ */
55
+ batch(operations: readonly DbBatchOp[]): Promise<void>;
46
56
  /** Close the underlying connection. */
47
57
  close(): void;
48
58
  }
49
59
 
60
+ /**
61
+ * A single write operation inside a {@link DbAdapter.batch} call.
62
+ */
63
+ export interface DbBatchOp {
64
+ /** SQL statement (INSERT/UPDATE/DELETE). */
65
+ sql: string;
66
+ /** Bind parameters for the statement. */
67
+ params: readonly unknown[];
68
+ }
69
+
50
70
  /**
51
71
  * Discriminated-union config for creating a database adapter (bun-sqlite or D1).
52
72
  */
@@ -1,7 +1,7 @@
1
1
  import { isAbsolutePath, resolvePath } from '@gobing-ai/ts-runtime';
2
2
  import { Database } from '@gobing-ai/ts-runtime/bun-sqlite';
3
3
  import { type BunSQLiteDatabase, drizzle } from 'drizzle-orm/bun-sqlite';
4
- import type { DbAdapter, InternalDb } from '../adapter';
4
+ import type { DbAdapter, DbBatchOp, InternalDb } from '../adapter';
5
5
  import * as schema from '../schema/runtime';
6
6
 
7
7
  type SqliteStatementLike = {
@@ -103,6 +103,16 @@ export class BunSqliteAdapter implements DbAdapter {
103
103
  return ((stmt as unknown as { all: (...p: unknown[]) => T[] }).all(...params) as T[]) ?? [];
104
104
  }
105
105
 
106
+ async batch(operations: readonly DbBatchOp[]): Promise<void> {
107
+ if (operations.length === 0) return;
108
+ this.sqlite.transaction(() => {
109
+ for (const op of operations) {
110
+ const stmt = this.getStatement(op.sql);
111
+ stmt.run(...op.params);
112
+ }
113
+ })();
114
+ }
115
+
106
116
  close(): void {
107
117
  this.sqlite.close();
108
118
  }
@@ -1,5 +1,5 @@
1
1
  import { type DrizzleD1Database, drizzle } from 'drizzle-orm/d1';
2
- import type { DbAdapter, InternalDb } from '../adapter';
2
+ import type { DbAdapter, DbBatchOp, InternalDb } from '../adapter';
3
3
  import * as schema from '../schema/runtime';
4
4
 
5
5
  /**
@@ -12,6 +12,14 @@ export interface D1Binding {
12
12
  run?(): Promise<{ results: unknown[]; success: boolean }>;
13
13
  };
14
14
  exec(sql: string): Promise<void>;
15
+ /**
16
+ * Run multiple statements as a single atomic batch (D1's native `batch()`
17
+ * API). Optional for single-statement test doubles only: when absent,
18
+ * {@link D1Adapter.batch} runs a lone statement directly (trivially atomic)
19
+ * and **throws** for multi-statement batches rather than silently degrading
20
+ * atomicity. Real D1 bindings always provide `batch()`.
21
+ */
22
+ batch?(statements: D1BoundStatement[]): Promise<unknown[]>;
15
23
  }
16
24
 
17
25
  interface D1BoundStatement {
@@ -76,6 +84,29 @@ export class D1Adapter implements DbAdapter {
76
84
  return result.results ?? [];
77
85
  }
78
86
 
87
+ async batch(operations: readonly DbBatchOp[]): Promise<void> {
88
+ if (operations.length === 0) return;
89
+ // A single statement is atomic on its own, so a binding without native
90
+ // batch() may run it directly. Multiple statements without native batch()
91
+ // cannot be made atomic — fail loud rather than silently break the
92
+ // all-or-nothing contract (real D1 bindings always provide batch()).
93
+ if (this.binding.batch === undefined && operations.length > 1) {
94
+ throw new Error(
95
+ `D1 binding has no batch() — cannot execute ${operations.length} statements atomically; ` +
96
+ 'provide a binding with native batch() support',
97
+ );
98
+ }
99
+ const boundStmts: D1BoundStatement[] = operations.map((op) => {
100
+ const stmt = this.binding.prepare(op.sql);
101
+ return op.params.length > 0 ? stmt.bind(...op.params) : stmt.bind();
102
+ });
103
+ if (this.binding.batch !== undefined) {
104
+ await this.binding.batch(boundStmts);
105
+ } else {
106
+ await boundStmts[0]?.run();
107
+ }
108
+ }
109
+
79
110
  close(): void {
80
111
  // D1 bindings are managed by the Workers runtime -- no-op
81
112
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type InternalDb } from './adapter';
1
+ export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type DbBatchOp, type InternalDb } from './adapter';
2
2
  export { D1Adapter } from './adapters/d1';
3
3
  export { BaseDao, type TxHandle } from './base-dao';
4
4