@supacloud/db 0.7.1 → 0.8.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 CHANGED
@@ -1,5 +1,52 @@
1
1
  # @supacloud/db
2
2
 
3
+ ## Drizzle, Read Queries and SQL Impact
4
+
5
+ Use `defineDrizzleDatabaseModule` and `createDrizzleCommandDatabase` from
6
+ `@supacloud/db/drizzle`, or `createBunDrizzleCommandDatabase` from
7
+ `@supacloud/db/drizzle-bun`. Authorization, business writes, receipts and audit
8
+ share one transaction connection. Do not use the outer pool for command writes
9
+ or retain `transaction.db` after the transaction. The adapter currently targets
10
+ `drizzle-orm@1.0.0-rc.4`, not a stable 1.0 release.
11
+
12
+ `executeDecodedSql` keeps SQL results unknown until an explicit decoder validates
13
+ them. Use parameterized templates; `sql<T>` alone does not validate returned data.
14
+ The optional `@supacloud/commands/typebox` contract can use schemas derived through
15
+ `drizzle-orm/typebox-legacy`, selecting public fields rather than accepting
16
+ internal tenant/permission fields.
17
+
18
+ `defineReadQuery` combines input decoding, authorization, result decoding and
19
+ payload-free observation. `createBunReadDatabase` admits one parsed SELECT and
20
+ runs a PostgreSQL READ ONLY transaction with a statement timeout.
21
+ `createBunDrizzleReadDatabase` provides the read transaction boundary for Drizzle.
22
+ Keep least-privilege roles and RLS; AOP does not replace them.
23
+
24
+ `supacloud-sql-impact impact.json --check` analyzes ordered, owned migrations:
25
+
26
+ ```json
27
+ {
28
+ "migrations": [
29
+ { "id": "001", "owner": "orders", "path": "migrations/001.sql" }
30
+ ],
31
+ "baseline": []
32
+ }
33
+ ```
34
+
35
+ Paths are relative to this config. Reports include fingerprints, dependencies,
36
+ transitive impact, review reasons and a digest. Persist applied IDs and SHA-256
37
+ hashes as `baseline`; deleted, reordered or rewritten history fails validation.
38
+ Supply `readSqlDependencyGraph(executor, ["app"])` results as `catalogEdges` and
39
+ `catalogReview`. Import this catalog reader from `@supacloud/db/sql-analysis`.
40
+
41
+ Dynamic SQL, opaque functions, unqualified names and unsupported semantics require
42
+ review. This is conservative analysis, not proof of arbitrary SQL safety. Approve
43
+ the exact reviewed report via `approvedDigest`; changes invalidate approval, and
44
+ approval never overrides history errors. The tool does not execute migrations.
45
+
46
+ The focused `tests/drizzle-foundation.test.ts` test needs Docker and starts a
47
+ disposable local PostgreSQL/PGMQ instance. Prepare Commands and Compiler package
48
+ dependencies first using `scripts/build-command-dependencies.ts db`.
49
+
3
50
  ## Durable Commands
4
51
 
5
52
  `createPostgresCommandStore(database)` implements the protocol's storage ports.
@@ -10,7 +57,8 @@ and input redaction. It does not own a recovery polling queue.
10
57
  Execution factories moved to `@supacloud/commands`; pass `store` instead of
11
58
  `database`. Errors moved to `CommandError` in `@supacloud/contracts`. Neither
12
59
  remote sending nor command orchestration is re-exported here. The DB package
13
- does not depend on the command runtime, including through its tests.
60
+ does not depend on the command runtime in production. The Drizzle integration
61
+ test uses the command runtime to verify the shared transaction boundary.
14
62
 
15
63
  Install the existing PGMQ/Workflow runtime and updated commands-public SQL module,
16
64
  then `COMMAND_PERSISTENCE_SQL` through a privileged application migration.
@@ -2,8 +2,8 @@ import type { CommandStore, CommandRetentionStore } from "@supacloud/contracts";
2
2
  export interface CommandTransaction {
3
3
  query(sql: string, parameters?: readonly (string | number | boolean | null)[]): Promise<unknown>;
4
4
  }
5
- export interface CommandDatabase {
6
- transaction<T>(run: (transaction: CommandTransaction) => Promise<T>): Promise<T>;
5
+ export interface CommandDatabase<Transaction extends CommandTransaction = CommandTransaction> {
6
+ transaction<T>(run: (transaction: Transaction) => Promise<T>): Promise<T>;
7
7
  }
8
8
  export interface CommandSubmissionBinding {
9
9
  commandId: string;
@@ -13,6 +13,6 @@ export interface CommandSubmissionBinding {
13
13
  workerId: string;
14
14
  }
15
15
  /** PostgreSQL only: no remote sending, domain matching or recovery policy lives here. */
16
- export declare function createPostgresCommandStore(database: CommandDatabase, options?: {
16
+ export declare function createPostgresCommandStore<Transaction extends CommandTransaction>(database: CommandDatabase<Transaction>, options?: {
17
17
  submission?: CommandSubmissionBinding;
18
- }): CommandStore<CommandTransaction> & CommandRetentionStore;
18
+ }): CommandStore<Transaction> & CommandRetentionStore;
@@ -2,3 +2,7 @@ import type { SQL } from "bun";
2
2
  import type { CommandDatabase } from "./command-adapter";
3
3
  /** The SQL pool owns one connection from BEGIN through COMMIT/ROLLBACK. */
4
4
  export declare function createBunCommandDatabase(sql: SQL): CommandDatabase;
5
+ /** The driver owns a read-only transaction and enforces its statement timeout. */
6
+ export declare function createBunReadDatabase(pool: SQL, timeoutMs?: number): Readonly<{
7
+ query(text: string, parameters?: readonly (string | number | boolean | null)[], signal?: AbortSignal): Promise<unknown>;
8
+ }>;
@@ -1,3 +1,287 @@
1
+ var __esm = (fn, res, err) => () => {
2
+ if (fn)
3
+ try {
4
+ res = fn(fn = 0);
5
+ } catch (e) {
6
+ err = [e];
7
+ }
8
+ if (err)
9
+ throw err[0];
10
+ return res;
11
+ };
12
+
13
+ // src/sql-analysis.ts
14
+ import { createHash } from "node:crypto";
15
+ import { fingerprint, parse } from "libpg-query";
16
+ function record(value) {
17
+ return value !== null && typeof value === "object" && !Array.isArray(value);
18
+ }
19
+ function walk(value, visit) {
20
+ if (Array.isArray(value)) {
21
+ for (const item of value)
22
+ walk(item, visit);
23
+ } else if (record(value)) {
24
+ visit(value);
25
+ for (const item of Object.values(value))
26
+ walk(item, visit);
27
+ }
28
+ }
29
+ function names(value) {
30
+ if (!Array.isArray(value))
31
+ return [];
32
+ return value.flatMap((part) => record(part) && record(part["String"]) && typeof part["String"]["sval"] === "string" ? [part["String"]["sval"]] : []);
33
+ }
34
+ async function analyzeSql(sql) {
35
+ const result = await parse(sql);
36
+ const defines = new Set, touches = new Set, references = new Set, review = new Set;
37
+ const qualified = (kind, parts) => {
38
+ if (parts.length !== 2) {
39
+ review.add(`Resolve unqualified or unsupported ${kind} name: ${parts.join(".")}`);
40
+ return;
41
+ }
42
+ return objectId(kind, parts);
43
+ };
44
+ const relation = (value) => {
45
+ if (!record(value) || typeof value["relname"] !== "string")
46
+ return;
47
+ return qualified("relation", typeof value["schemaname"] === "string" ? [value["schemaname"], value["relname"]] : [value["relname"]]);
48
+ };
49
+ walk(result, (node) => {
50
+ if (record(node["RangeVar"])) {
51
+ const name = relation(node["RangeVar"]);
52
+ if (name)
53
+ references.add(name);
54
+ }
55
+ if (record(node["FuncCall"])) {
56
+ const name = qualified("function", names(node["FuncCall"]["funcname"]));
57
+ if (name)
58
+ references.add(name);
59
+ }
60
+ if (record(node["Constraint"])) {
61
+ const name = relation(node["Constraint"]["pktable"]);
62
+ if (name)
63
+ references.add(name);
64
+ }
65
+ });
66
+ const statements = record(result) && Array.isArray(result["stmts"]) ? result["stmts"] : [];
67
+ for (const raw of statements) {
68
+ if (!record(raw) || !record(raw["stmt"]))
69
+ throw new Error("Invalid PostgreSQL parse tree");
70
+ const [kind, node] = Object.entries(raw["stmt"])[0] ?? [];
71
+ if (!kind || !record(node))
72
+ throw new Error("Invalid SQL statement");
73
+ const target = relation(node["relation"] ?? node["view"] ?? node["table"]);
74
+ if (target) {
75
+ touches.add(target);
76
+ if (kind === "CreateStmt" || kind === "ViewStmt")
77
+ defines.add(target);
78
+ }
79
+ if (kind === "DropStmt") {
80
+ for (const item of Array.isArray(node["objects"]) ? node["objects"] : []) {
81
+ const name = record(item) && record(item["List"]) ? qualified(node["removeType"] === "OBJECT_TABLE" || node["removeType"] === "OBJECT_VIEW" ? "relation" : "object", names(item["List"]["items"])) : undefined;
82
+ if (name)
83
+ touches.add(name);
84
+ else
85
+ review.add("Review dropped object identity and dependencies");
86
+ }
87
+ review.add("Destructive DROP requires review");
88
+ } else if (kind === "CreateFunctionStmt") {
89
+ const name = qualified("function", names(node["funcname"]));
90
+ if (name) {
91
+ defines.add(name);
92
+ touches.add(name);
93
+ }
94
+ let language, body;
95
+ for (const item of Array.isArray(node["options"]) ? node["options"] : []) {
96
+ if (!record(item) || !record(item["DefElem"]))
97
+ continue;
98
+ const option = item["DefElem"];
99
+ if (option["defname"] === "language")
100
+ language = names([option["arg"]])[0];
101
+ if (option["defname"] === "as" && record(option["arg"]) && record(option["arg"]["List"]))
102
+ body = names(option["arg"]["List"]["items"])[0];
103
+ }
104
+ if (language === "sql" && body !== undefined) {
105
+ const nested = await analyzeSql(body);
106
+ for (const name of [...nested.references, ...nested.touches])
107
+ references.add(name);
108
+ for (const item of nested.review)
109
+ review.add(item);
110
+ } else {
111
+ review.add("Function body dependencies require catalog verification or explicit reviewed dependencies");
112
+ }
113
+ review.add("Review function security, grants and overloads");
114
+ } else if (![
115
+ "SelectStmt",
116
+ "CreateStmt",
117
+ "ViewStmt",
118
+ "AlterTableStmt",
119
+ "IndexStmt",
120
+ "InsertStmt",
121
+ "UpdateStmt",
122
+ "DeleteStmt",
123
+ "CreatePolicyStmt",
124
+ "AlterPolicyStmt",
125
+ "CreateTrigStmt"
126
+ ].includes(kind)) {
127
+ review.add(`Review unsupported dependency semantics: ${kind}`);
128
+ }
129
+ if (["AlterTableStmt", "CreatePolicyStmt", "AlterPolicyStmt", "CreateTrigStmt"].includes(kind)) {
130
+ review.add(`Review schema/access impact: ${kind}`);
131
+ }
132
+ }
133
+ return {
134
+ fingerprint: await fingerprint(sql),
135
+ defines: [...defines].sort(),
136
+ touches: [...touches].sort(),
137
+ references: [...references].sort(),
138
+ review: [...review].sort()
139
+ };
140
+ }
141
+ async function assertReadSql(sql) {
142
+ const tree = await parse(sql);
143
+ if (!record(tree) || !Array.isArray(tree["stmts"]) || tree["stmts"].length !== 1)
144
+ throw new Error("Expected one read statement");
145
+ const entry = tree["stmts"][0];
146
+ if (!record(entry) || !record(entry["stmt"]) || !record(entry["stmt"]["SelectStmt"]))
147
+ throw new Error("Expected SELECT");
148
+ walk(tree, (node) => {
149
+ if (["InsertStmt", "UpdateStmt", "DeleteStmt", "MergeStmt", "IntoClause", "intoClause", "LockingClause"].some((name) => (name in node))) {
150
+ throw new Error("Mutating or locking SQL is not a read query");
151
+ }
152
+ });
153
+ return fingerprint(sql);
154
+ }
155
+ async function readSqlDependencyGraph(executor, schemas) {
156
+ if (!schemas.length || schemas.some((schema) => !schema.trim()))
157
+ throw new TypeError("Select explicit application schemas");
158
+ const schemaParameters = schemas.map((_, index) => `$${index + 1}::text`).join(", ");
159
+ const rows = await executor.query(`
160
+ WITH objects AS (
161
+ SELECT 'pg_class'::regclass::oid AS classid, c.oid AS objid, n.nspname,
162
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text AS identity
163
+ FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
164
+ UNION ALL
165
+ SELECT 'pg_proc'::regclass::oid, p.oid, n.nspname,
166
+ 'function:' || to_json(n.nspname)::text || '.' || to_json(p.proname)::text
167
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
168
+ UNION ALL
169
+ SELECT 'pg_rewrite'::regclass::oid, r.oid, n.nspname,
170
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
171
+ FROM pg_rewrite r JOIN pg_class c ON c.oid=r.ev_class JOIN pg_namespace n ON n.oid=c.relnamespace
172
+ UNION ALL
173
+ SELECT 'pg_policy'::regclass::oid, p.oid, n.nspname,
174
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
175
+ FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid JOIN pg_namespace n ON n.oid=c.relnamespace
176
+ UNION ALL
177
+ SELECT 'pg_trigger'::regclass::oid, t.oid, n.nspname,
178
+ 'relation:' || to_json(n.nspname)::text || '.' || to_json(c.relname)::text
179
+ FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace
180
+ )
181
+ SELECT DISTINCT a.identity AS dependent, b.identity AS dependency
182
+ FROM pg_depend d
183
+ JOIN objects a ON a.classid=d.classid AND a.objid=d.objid
184
+ JOIN objects b ON b.classid=d.refclassid AND b.objid=d.refobjid
185
+ WHERE a.nspname IN (${schemaParameters})
186
+ AND b.nspname IN (${schemaParameters}) AND a.identity<>b.identity
187
+ ORDER BY dependent, dependency`, [...schemas]);
188
+ if (!Array.isArray(rows))
189
+ throw new TypeError("Invalid PostgreSQL dependency rows");
190
+ const edges = rows.map((row) => {
191
+ if (!record(row) || typeof row["dependent"] !== "string" || typeof row["dependency"] !== "string")
192
+ throw new TypeError("Invalid PostgreSQL dependency edge");
193
+ return { dependent: row["dependent"], dependency: row["dependency"] };
194
+ });
195
+ const functions = await executor.query(`
196
+ SELECT DISTINCT n.nspname AS schema, p.proname AS name
197
+ FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
198
+ JOIN pg_language l ON l.oid=p.prolang
199
+ WHERE n.nspname IN (${schemaParameters})
200
+ AND (l.lanname='plpgsql' OR (l.lanname='sql' AND p.prosqlbody IS NULL))
201
+ ORDER BY schema, name`, [...schemas]);
202
+ if (!Array.isArray(functions))
203
+ throw new TypeError("Invalid PostgreSQL function rows");
204
+ return {
205
+ edges,
206
+ review: functions.map((row) => {
207
+ if (!record(row) || typeof row["schema"] !== "string" || typeof row["name"] !== "string")
208
+ throw new TypeError("Invalid function identity");
209
+ return `Catalog cannot prove function-body dependencies: ${objectId("function", [row["schema"], row["name"]])}`;
210
+ })
211
+ };
212
+ }
213
+ async function planSqlImpact(sources, baseline = [], catalogEdges = []) {
214
+ const ids = new Set;
215
+ const migrations = [];
216
+ const edges = new Map;
217
+ const addEdge = (dependent, dependency) => {
218
+ if (dependent !== dependency)
219
+ edges.set(JSON.stringify([dependent, dependency]), { dependent, dependency });
220
+ };
221
+ for (const edge of catalogEdges)
222
+ addEdge(edge["dependent"], edge["dependency"]);
223
+ for (const source of sources) {
224
+ if (!source.id.trim() || !source.owner.trim() || ids.has(source.id))
225
+ throw new Error("Migration IDs must be unique and each migration must have an owner");
226
+ ids.add(source.id);
227
+ const analysis = await analyzeSql(source.sql);
228
+ const references = [...new Set([...analysis.references, ...source.dependencies ?? []])].sort();
229
+ for (const target of analysis.touches)
230
+ for (const reference of references)
231
+ addEdge(target, reference);
232
+ migrations.push({
233
+ id: source.id,
234
+ owner: source.owner,
235
+ sha256: createHash("sha256").update(source.sql).digest("hex"),
236
+ analysis: { ...analysis, references }
237
+ });
238
+ }
239
+ const current = new Map(migrations.map((entry) => [entry.id, entry]));
240
+ const previous = new Map(baseline.map((entry) => [entry.id, entry]));
241
+ const errors = [];
242
+ if (previous.size !== baseline.length)
243
+ errors.push("Duplicate migration in baseline");
244
+ for (const [index, old] of baseline.entries()) {
245
+ if (current.get(old.id)?.sha256 !== old.sha256 || migrations[index]?.id !== old.id) {
246
+ errors.push(`Applied migration removed, reordered or rewritten: ${old.id}`);
247
+ }
248
+ }
249
+ const changed = migrations.filter((entry) => previous.get(entry.id)?.sha256 !== entry.sha256);
250
+ const affected = new Set(changed.flatMap((entry) => entry.analysis.touches));
251
+ const reverse = new Map;
252
+ for (const edge of edges.values()) {
253
+ const values = reverse.get(edge["dependency"]) ?? [];
254
+ values.push(edge["dependent"]);
255
+ reverse.set(edge["dependency"], values);
256
+ }
257
+ for (const object of affected)
258
+ for (const dependent of reverse.get(object) ?? [])
259
+ affected.add(dependent);
260
+ const definitions = new Map;
261
+ migrations.forEach((entry, index) => {
262
+ for (const object of entry.analysis.defines)
263
+ if (!definitions.has(object))
264
+ definitions.set(object, index);
265
+ });
266
+ migrations.forEach((entry, index) => {
267
+ for (const reference of entry.analysis.references) {
268
+ if ((definitions.get(reference) ?? -1) > index)
269
+ errors.push(`${entry.id} references an object defined by a later migration: ${reference}`);
270
+ }
271
+ });
272
+ return {
273
+ migrations,
274
+ edges: [...edges.values()].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
275
+ changed: changed.map((entry) => entry.id),
276
+ affectedObjects: [...affected].sort(),
277
+ affectedMigrations: migrations.filter((entry) => [...entry.analysis.touches, ...entry.analysis.references].some((object) => affected.has(object))).map((entry) => entry.id),
278
+ errors,
279
+ review: changed.flatMap((entry) => entry.analysis.review.map((note) => `${entry.id}: ${note}`))
280
+ };
281
+ }
282
+ var objectId = (kind, parts) => `${kind}:${parts.map((part) => JSON.stringify(part)).join(".")}`;
283
+ var init_sql_analysis = () => {};
284
+
1
285
  // src/command-bun.ts
2
286
  function createBunCommandDatabase(sql) {
3
287
  return {
@@ -14,6 +298,53 @@ function createBunCommandDatabase(sql) {
14
298
  }
15
299
  };
16
300
  }
301
+ function createBunReadDatabase(pool, timeoutMs = 5000) {
302
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) {
303
+ throw new TypeError("Invalid read statement timeout");
304
+ }
305
+ const admitted = new Map;
306
+ return Object.freeze({
307
+ async query(text, parameters = [], signal) {
308
+ if (signal?.aborted)
309
+ throw new Error("Read aborted");
310
+ await Promise.resolve().then(() => init_sql_analysis());
311
+ let admission = admitted.get(text);
312
+ if (!admission) {
313
+ admission = assertReadSql(text);
314
+ if (admitted.size >= 256) {
315
+ const oldest = admitted.keys().next().value;
316
+ if (oldest !== undefined)
317
+ admitted.delete(oldest);
318
+ }
319
+ admitted.set(text, admission);
320
+ }
321
+ try {
322
+ await admission;
323
+ } catch (error) {
324
+ admitted.delete(text);
325
+ throw error;
326
+ }
327
+ return pool.begin(async (connection) => {
328
+ await connection.unsafe("SET TRANSACTION READ ONLY");
329
+ await connection`SELECT set_config('statement_timeout', ${String(timeoutMs)}, true)`;
330
+ if (signal?.aborted)
331
+ throw new Error("Read aborted");
332
+ const pending = connection.unsafe(text, [...parameters]);
333
+ const abort = () => pending.cancel();
334
+ signal?.addEventListener("abort", abort, { once: true });
335
+ try {
336
+ const rows = await pending;
337
+ if (signal?.aborted)
338
+ throw new Error("Read aborted");
339
+ return rows;
340
+ } finally {
341
+ signal?.removeEventListener("abort", abort);
342
+ }
343
+ });
344
+ }
345
+ });
346
+ }
17
347
  export {
18
- createBunCommandDatabase
348
+ createBunCommandDatabase,
349
+ createBunReadDatabase
19
350
  };
@@ -0,0 +1,10 @@
1
+ import type { SQL } from "bun";
2
+ import { type BunSQLDatabase } from "drizzle-orm/bun-sql/postgres";
3
+ import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations";
4
+ import type { DrizzlePgConfig } from "drizzle-orm/pg-core";
5
+ /** Drizzle is bound to Bun's pinned transaction client, not the application pool. */
6
+ export declare function createBunDrizzleCommandDatabase<Relations extends AnyRelations = EmptyRelations>(pool: SQL, config?: DrizzlePgConfig<Relations>): import("./command-adapter").CommandDatabase<import("./drizzle").DrizzleCommandTransaction<BunSQLDatabase<Relations>>>;
7
+ /** Explicit read-only unit of work; use a least-privilege pool with the intended RLS identity. */
8
+ export declare function createBunDrizzleReadDatabase<Relations extends AnyRelations = EmptyRelations>(pool: SQL, config?: DrizzlePgConfig<Relations>, timeoutMs?: number): Readonly<{
9
+ read<Result>(work: (db: BunSQLDatabase<Relations>) => Promise<Result>): Promise<Result>;
10
+ }>;
@@ -0,0 +1,93 @@
1
+ // src/drizzle-bun.ts
2
+ import { drizzle } from "drizzle-orm/bun-sql/postgres";
3
+
4
+ // src/drizzle.ts
5
+ import { is } from "drizzle-orm";
6
+ import { getTableConfig, PgTable } from "drizzle-orm/pg-core";
7
+
8
+ // src/module.ts
9
+ function normalizeTable(ref) {
10
+ if (typeof ref === "string")
11
+ return ref;
12
+ const schema = ref._.schema ?? "public";
13
+ return `${schema}.${ref._.name}`;
14
+ }
15
+ function defineDatabaseModule(options) {
16
+ return {
17
+ name: options.name,
18
+ tables: (options.tables ?? []).map(normalizeTable),
19
+ policies: options.policies ?? [],
20
+ functions: options.functions ?? [],
21
+ triggers: options.triggers ?? [],
22
+ grants: options.grants ?? []
23
+ };
24
+ }
25
+
26
+ // src/drizzle.ts
27
+ function defineDrizzleDatabaseModule(tables, options) {
28
+ const names = new Set;
29
+ for (const table of Object.values(tables)) {
30
+ if (!is(table, PgTable))
31
+ throw new TypeError("Expected a PostgreSQL Drizzle table");
32
+ const config = getTableConfig(table);
33
+ names.add(`${config.schema ?? "public"}.${config.name}`);
34
+ }
35
+ return defineDatabaseModule({ ...options, tables: [...names].sort() });
36
+ }
37
+ function createDrizzleCommandDatabase(options) {
38
+ return {
39
+ transaction: (work) => options.transaction(async (connection) => {
40
+ const db = options.bind(connection);
41
+ let active = true;
42
+ try {
43
+ return await work({
44
+ db,
45
+ query: async (text, parameters = []) => {
46
+ if (!active)
47
+ throw new Error("Command transaction is closed");
48
+ return options.query(connection, text, parameters);
49
+ }
50
+ });
51
+ } finally {
52
+ active = false;
53
+ }
54
+ })
55
+ };
56
+ }
57
+ async function executeDecodedSql(executor, query, decode) {
58
+ return decode(await executor.execute(query));
59
+ }
60
+
61
+ // src/drizzle-bun.ts
62
+ function createBunDrizzleCommandDatabase(pool, config = {}) {
63
+ return createDrizzleCommandDatabase({
64
+ transaction: async (work) => {
65
+ const result = await pool.begin(async (connection) => ({ value: await work(connection) }));
66
+ return result.value;
67
+ },
68
+ bind: (connection) => drizzle({ ...config, client: connection }),
69
+ query: async (connection, text, parameters) => {
70
+ const rows = await connection.unsafe(text, [...parameters]);
71
+ return rows;
72
+ }
73
+ });
74
+ }
75
+ function createBunDrizzleReadDatabase(pool, config = {}, timeoutMs = 5000) {
76
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) {
77
+ throw new TypeError("Invalid read statement timeout");
78
+ }
79
+ return Object.freeze({
80
+ async read(work) {
81
+ const result = await pool.begin(async (connection) => {
82
+ await connection.unsafe("SET TRANSACTION READ ONLY");
83
+ await connection`SELECT set_config('statement_timeout', ${String(timeoutMs)}, true)`;
84
+ return { value: await work(drizzle({ ...config, client: connection })) };
85
+ });
86
+ return result.value;
87
+ }
88
+ });
89
+ }
90
+ export {
91
+ createBunDrizzleCommandDatabase,
92
+ createBunDrizzleReadDatabase
93
+ };
@@ -0,0 +1,26 @@
1
+ import { type SQL } from "drizzle-orm";
2
+ import { PgTable } from "drizzle-orm/pg-core";
3
+ import type { DatabaseModule, DatabaseModuleOptions } from "./module.js";
4
+ import type { CommandDatabase, CommandTransaction } from "./command-adapter";
5
+ /** Accept explicit table exports, not arbitrary objects or private type-only metadata. */
6
+ export declare function defineDrizzleDatabaseModule(tables: Readonly<Record<string, PgTable>>, options: Omit<DatabaseModuleOptions, "tables">): DatabaseModule;
7
+ export interface DrizzleCommandTransaction<Database> extends CommandTransaction {
8
+ readonly db: Database;
9
+ }
10
+ export interface DrizzleCommandDatabaseOptions<Connection, Database> {
11
+ /** Owns BEGIN/COMMIT/ROLLBACK and pins one connection for the callback. */
12
+ transaction<T>(work: (connection: Connection) => Promise<T>): Promise<T>;
13
+ /** Construct Drizzle on this connection, never on the outer connection pool. */
14
+ bind(connection: NoInfer<Connection>): Database;
15
+ /** Execute parameterized protocol SQL on the same connection; return rows only. */
16
+ query(connection: NoInfer<Connection>, text: string, parameters: readonly (string | number | boolean | null)[]): Promise<unknown>;
17
+ }
18
+ /**
19
+ * One native transaction serves Drizzle business writes and SQL receipts.
20
+ * No SQL placeholder rewriting, hidden second transaction or driver internals.
21
+ */
22
+ export declare function createDrizzleCommandDatabase<Connection, Database>(options: DrizzleCommandDatabaseOptions<Connection, Database>): CommandDatabase<DrizzleCommandTransaction<Database>>;
23
+ /** Raw SQL results stay unknown until an application-owned decoder validates them. */
24
+ export declare function executeDecodedSql<Result>(executor: {
25
+ execute(query: SQL): PromiseLike<unknown>;
26
+ }, query: SQL<unknown>, decode: (value: unknown) => Result): Promise<Result>;
@@ -0,0 +1,61 @@
1
+ // src/drizzle.ts
2
+ import { is } from "drizzle-orm";
3
+ import { getTableConfig, PgTable } from "drizzle-orm/pg-core";
4
+
5
+ // src/module.ts
6
+ function normalizeTable(ref) {
7
+ if (typeof ref === "string")
8
+ return ref;
9
+ const schema = ref._.schema ?? "public";
10
+ return `${schema}.${ref._.name}`;
11
+ }
12
+ function defineDatabaseModule(options) {
13
+ return {
14
+ name: options.name,
15
+ tables: (options.tables ?? []).map(normalizeTable),
16
+ policies: options.policies ?? [],
17
+ functions: options.functions ?? [],
18
+ triggers: options.triggers ?? [],
19
+ grants: options.grants ?? []
20
+ };
21
+ }
22
+
23
+ // src/drizzle.ts
24
+ function defineDrizzleDatabaseModule(tables, options) {
25
+ const names = new Set;
26
+ for (const table of Object.values(tables)) {
27
+ if (!is(table, PgTable))
28
+ throw new TypeError("Expected a PostgreSQL Drizzle table");
29
+ const config = getTableConfig(table);
30
+ names.add(`${config.schema ?? "public"}.${config.name}`);
31
+ }
32
+ return defineDatabaseModule({ ...options, tables: [...names].sort() });
33
+ }
34
+ function createDrizzleCommandDatabase(options) {
35
+ return {
36
+ transaction: (work) => options.transaction(async (connection) => {
37
+ const db = options.bind(connection);
38
+ let active = true;
39
+ try {
40
+ return await work({
41
+ db,
42
+ query: async (text, parameters = []) => {
43
+ if (!active)
44
+ throw new Error("Command transaction is closed");
45
+ return options.query(connection, text, parameters);
46
+ }
47
+ });
48
+ } finally {
49
+ active = false;
50
+ }
51
+ })
52
+ };
53
+ }
54
+ async function executeDecodedSql(executor, query, decode) {
55
+ return decode(await executor.execute(query));
56
+ }
57
+ export {
58
+ createDrizzleCommandDatabase,
59
+ defineDrizzleDatabaseModule,
60
+ executeDecodedSql
61
+ };
package/dist/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export { buildDatabaseManifest, explainObject, type DatabaseManifest, type Datab
9
9
  export { createDatabaseAccessBoundary, DatabaseAccessError, type AuthenticatedDatabaseIdentity, type DatabaseAccessBoundary, type DatabaseAccessBoundaryOptions, type DatabaseAccessErrorCode, } from './access.js';
10
10
  export { createRpcClient, defineRpcContract, RpcContractError, type RpcArgs, type RpcCallResult, type RpcContract, type RpcDecoder, type RpcResult, type RpcTransport, } from './rpc.js';
11
11
  export { COMMAND_PERSISTENCE_SQL, COMMAND_PERSISTENCE_UPGRADE_SQL } from "./command-schema";
12
+ export { defineReadQuery, decodePostgrestQuery, QueryBoundaryError, type QueryEvent, type QueryObserver, type ReadQueryDefinition } from "./query";
12
13
  export { createPostgresCommandStore, type CommandDatabase, type CommandTransaction, type CommandSubmissionBinding, } from "./command-adapter";