@venn-lang/db 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @venn-lang/db
2
+
3
+ > The `db` namespace: seed tables, run statements and snapshot state, all through the `DbClient` port.
4
+
5
+ A flow that talks to a database needs the rows to be there before the first assertion and gone
6
+ after the last. This package gives six verbs for that, and puts every one of them behind a typed
7
+ port so the same flow runs against in-memory tables or a real connection without a line changing.
8
+
9
+ ## Install
10
+
11
+ The package is part of the stdlib the `venn` CLI loads, so nothing to install. Reach it from a
12
+ flow with a `use` line:
13
+
14
+ ```ruby
15
+ use "venn/db"
16
+ ```
17
+
18
+ The plugin declares the `net` capability. A host that does not offer it fails the load with a
19
+ readable diagnostic rather than a `TypeError` mid-run.
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ module demo.orders
25
+
26
+ use "venn/assert"
27
+ use "venn/db"
28
+
29
+ flow "Orders survive a snapshot" {
30
+ step "seed the tables" {
31
+ const loaded = db.seed { users: [{ id: 1, name: "Ada" }, { id: 2, name: "Grace" }] }
32
+ expect loaded == 2
33
+ }
34
+
35
+ step "query them back" {
36
+ const rows = db.query "SELECT * FROM users"
37
+ expect rows.len == 2
38
+
39
+ const grace = db.query "SELECT * FROM users" { where: { id: 2 } }
40
+ expect grace[0].name == "Grace"
41
+ }
42
+
43
+ step "a snapshot undoes a mutation" {
44
+ const before = db.snapshot
45
+ const cleared = db.exec "TRUNCATE users"
46
+ expect cleared == 2
47
+
48
+ db.restore before
49
+ const rows = db.query "SELECT * FROM users"
50
+ expect rows.len == 2
51
+ }
52
+ }
53
+ ```
54
+
55
+ ## Verbs
56
+
57
+ | Verb | Options | Gives back |
58
+ | --- | --- | --- |
59
+ | `db.connect url` | none | The URL it connected to, as a string. |
60
+ | `db.query sql` | `where` (a row of column to value) | `list<db.Row>`, the matching rows. |
61
+ | `db.exec sql` | `rows` (the rows an INSERT carries) | `number`, how many rows were affected. |
62
+ | `db.seed tables` | none | `number`, how many rows were loaded. |
63
+ | `db.snapshot` | none | `db.Tables`, a copy of the current state. |
64
+ | `db.restore snapshot` | none | nothing. |
65
+
66
+ The statement is always the positional argument; everything else is an option in the trailing
67
+ map. `db.seed` accepts the tables either positionally or written inline as options, so
68
+ `db.seed { users: [...] }` and `db.seed baseline` both work.
69
+
70
+ Two named types are published: `db.Row` (a map of column name to value) and `db.Tables` (rows
71
+ grouped by table name, which is what `db.seed` takes and `db.snapshot` gives back).
72
+
73
+ ## The DbClient port
74
+
75
+ `venn.port.db-client`, contract version 1, requires `net`, six methods: `connect`, `query`,
76
+ `exec`, `seed`, `snapshot`, `restore`. Actions reach it with `ctx.port(DbClientPort)`, so nothing
77
+ in this package imports a driver.
78
+
79
+ Two implementations ship, and both answer the same conformance suite:
80
+
81
+ | Implementation | Behaviour |
82
+ | --- | --- |
83
+ | `createFakeDbClient({ tables })` | In-memory tables. Deterministic, offline, seedable. This is what the stdlib binds by default. |
84
+ | `createRealDbClient()` | A stub in this build. Every method throws a `VennError` with code `VN8090` rather than failing quietly. |
85
+
86
+ The fake carries a deliberately small query engine: `query` reads the table named after `FROM`
87
+ and filters by equality on each key of `where`; `exec` appends `rows` for `INSERT INTO` and clears
88
+ the table for `TRUNCATE` or `DELETE FROM`, returning the affected count. `snapshot` does a
89
+ structural clone, so restoring never aliases live state.
90
+
91
+ ## API
92
+
93
+ | Export | What it is |
94
+ | --- | --- |
95
+ | `dbPlugin` (also the default export) | The `PluginDefinition` for the `db` namespace. |
96
+ | `DbClientPort` | The port descriptor. |
97
+ | `DbClient` | The interface an implementation satisfies. |
98
+ | `createFakeDbClient`, `createRealDbClient` | The two implementations. |
99
+ | `QueryArgs`, `ExecArgs` | The argument objects `query` and `exec` take. |
100
+ | `TableMap`, `SeedData`, `DbSnapshot` | Rows keyed by table name. The last two are aliases of the first, named for where they are used. |
101
+ | `Row`, `RowSchema` | The nominal row type and its Zod schema. |
102
+ | `dbTypeDefs` | The `db.Row` and `db.Tables` specs the checker and the editor read. |
103
+
104
+ ## Adding an implementation
105
+
106
+ Write the file, add one line to `src/clients/index.ts`, and add one line to
107
+ `src/clients/db-client.test.ts` calling `dbClientConformance` with it. The suite is not rewritten;
108
+ if the new client passes it, the verbs above already work against it.
109
+
110
+ ## See also
111
+
112
+ - [`@venn-lang/data`](../std-data) for the deterministic rows to seed with.
113
+ - [`@venn-lang/contracts`](../contracts) for `Port`, `Host` and capability negotiation.
114
+ - [`@venn-lang/stdlib`](../stdlib) for the list of plugins and the port bindings the CLI runs with.
@@ -0,0 +1,89 @@
1
+ import { Port } from "@venn-lang/contracts";
2
+ import { PluginDefinition, ZodType } from "@venn-lang/sdk";
3
+ import { TypeSpec } from "@venn-lang/types";
4
+ //#region src/types/row.d.ts
5
+ /** The nominal `Row` type: an opaque record of column values. */
6
+ type Row = Record<string, unknown>;
7
+ /** Zod schema for the nominal `Row` type the plugin contributes. */
8
+ declare const RowSchema: ZodType<Row>;
9
+ //#endregion
10
+ //#region src/types/type-defs.d.ts
11
+ /**
12
+ * The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.
13
+ *
14
+ * Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which
15
+ * `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.
16
+ */
17
+ declare const dbTypeDefs: Readonly<Record<string, TypeSpec>>;
18
+ //#endregion
19
+ //#region src/port/db-client.types.d.ts
20
+ /** Arguments for {@link DbClient.query}: a SQL string plus an optional filter. */
21
+ interface QueryArgs {
22
+ sql: string;
23
+ where?: Row;
24
+ }
25
+ /** Arguments for {@link DbClient.exec}: a mutating statement plus optional rows. */
26
+ interface ExecArgs {
27
+ sql: string;
28
+ rows?: Row[];
29
+ }
30
+ /** Rows keyed by table name. The shape seeds and snapshots share. */
31
+ type TableMap = Record<string, Row[]>;
32
+ /** Rows to load, keyed by table name. */
33
+ type SeedData = TableMap;
34
+ /** A captured copy of every table. Detached from live state, so it can be restored later. */
35
+ type DbSnapshot = TableMap;
36
+ /**
37
+ * The contract a database is reached through. Actions call it via
38
+ * `ctx.port(DbClientPort)` and never touch a driver directly.
39
+ */
40
+ interface DbClient {
41
+ connect(url: string): Promise<void>;
42
+ query(args: QueryArgs): Promise<Row[]>;
43
+ exec(args: ExecArgs): Promise<number>;
44
+ seed(data: SeedData): Promise<number>;
45
+ snapshot(): Promise<DbSnapshot>;
46
+ restore(snapshot: DbSnapshot): Promise<void>;
47
+ }
48
+ //#endregion
49
+ //#region src/port/db-client.port.d.ts
50
+ /**
51
+ * The port every `db` verb reaches the database through.
52
+ *
53
+ * Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires
54
+ * the `net` capability, so a host without it is refused at load time rather
55
+ * than failing mid-run.
56
+ */
57
+ declare const DbClientPort: Port<DbClient>;
58
+ //#endregion
59
+ //#region src/clients/fake-client.d.ts
60
+ /**
61
+ * The in-memory `DbClient`: deterministic tables, no connection, no I/O.
62
+ *
63
+ * @param args.tables Rows to start from. Copied, so the caller's object is never mutated.
64
+ */
65
+ declare function createFakeDbClient(args?: {
66
+ tables?: SeedData;
67
+ }): DbClient;
68
+ //#endregion
69
+ //#region src/clients/real-client.d.ts
70
+ /**
71
+ * The driver-backed `DbClient`. Not implemented in this build.
72
+ *
73
+ * @throws VennError `VN8090` from every method, so a host that binds it fails
74
+ * loudly instead of quietly answering with nothing.
75
+ */
76
+ declare function createRealDbClient(): DbClient;
77
+ //#endregion
78
+ //#region src/plugin.d.ts
79
+ /**
80
+ * The `db` plugin: connect, query, exec, seed, snapshot and restore.
81
+ *
82
+ * Every verb reaches the database through `DbClientPort`, so the host decides
83
+ * whether that is the fake or a real driver. Loading fails up front unless the
84
+ * host offers the `net` capability.
85
+ */
86
+ declare const dbPlugin: PluginDefinition;
87
+ //#endregion
88
+ export { type DbClient, DbClientPort, type DbSnapshot, type ExecArgs, type QueryArgs, type Row, RowSchema, type SeedData, type TableMap, createFakeDbClient, createRealDbClient, dbPlugin, dbPlugin as default, dbTypeDefs };
89
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/types/row.ts","../src/types/type-defs.ts","../src/port/db-client.types.ts","../src/port/db-client.port.ts","../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/plugin.ts"],"mappings":";;;;;KAGY,MAAM;;cAGL,WAAW,QAAQ;;;;;;;;;cCEnB,YAAY,SAAS,eAAe;;;;UCLhC;EACf;EACA,QAAQ;;;UAIO;EACf;EACA,OAAO;;;KAIG,WAAW,eAAe;;KAG1B,WAAW;;KAGX,aAAa;;;;;UAMR;EACf,QAAQ,cAAc;EACtB,MAAM,MAAM,YAAY,QAAQ;EAChC,KAAK,MAAM,WAAW;EACtB,KAAK,MAAM,WAAW;EACtB,YAAY,QAAQ;EACpB,QAAQ,UAAU,aAAa;;;;;;;;;;;cCvBpB,cAAc,KAAK;;;;;;;;iBCEhB,mBAAmB;EAAQ,SAAS;IAAkB;;;;;;;;;iBCHtD,sBAAsB;;;;;;;;;;cCEzB,UAAU"}
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import { arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
3
+ import { t } from "@venn-lang/types";
4
+ //#region src/clients/query-engine.ts
5
+ /** Deep copy of the tables. A snapshot that aliased live state would follow later edits. */
6
+ function cloneTables(tables) {
7
+ return structuredClone(tables);
8
+ }
9
+ /** A tiny SELECT: pick the table named after `FROM`, optionally filter by `where`. */
10
+ function selectRows(tables, query) {
11
+ const rows = tables[tableName(query.sql, /from\s+([a-z_]\w*)/i)] ?? [];
12
+ const where = query.where;
13
+ return (where ? rows.filter((row) => matchesWhere(row, where)) : rows).map((row) => ({ ...row }));
14
+ }
15
+ /** A tiny mutation: INSERT appends rows; TRUNCATE/DELETE clears. Returns the count. */
16
+ function execStatement(tables, statement) {
17
+ const inserted = tableName(statement.sql, /insert\s+into\s+([a-z_]\w*)/i);
18
+ if (inserted) return insertRows(tables, inserted, statement.rows ?? []);
19
+ return truncate(tables, tableName(statement.sql, /(?:truncate|delete\s+from)\s+([a-z_]\w*)/i));
20
+ }
21
+ /** Append rows into named tables; returns how many rows were loaded. */
22
+ function seedTables(tables, data) {
23
+ let count = 0;
24
+ for (const [name, rows] of Object.entries(data)) {
25
+ const copies = rows.map((row) => ({ ...row }));
26
+ tables[name] = [...tables[name] ?? [], ...copies];
27
+ count += copies.length;
28
+ }
29
+ return count;
30
+ }
31
+ function insertRows(tables, name, rows) {
32
+ const copies = rows.map((row) => ({ ...row }));
33
+ tables[name] = [...tables[name] ?? [], ...copies];
34
+ return copies.length;
35
+ }
36
+ function truncate(tables, name) {
37
+ const removed = tables[name]?.length ?? 0;
38
+ if (name) tables[name] = [];
39
+ return removed;
40
+ }
41
+ function matchesWhere(row, where) {
42
+ return Object.entries(where).every(([key, value]) => row[key] === value);
43
+ }
44
+ function tableName(sql, pattern) {
45
+ return pattern.exec(sql)?.[1] ?? "";
46
+ }
47
+ //#endregion
48
+ //#region src/clients/fake-client.ts
49
+ /**
50
+ * The in-memory `DbClient`: deterministic tables, no connection, no I/O.
51
+ *
52
+ * @param args.tables Rows to start from. Copied, so the caller's object is never mutated.
53
+ */
54
+ function createFakeDbClient(args = {}) {
55
+ const state = { tables: cloneTables(args.tables ?? {}) };
56
+ return {
57
+ connect: () => Promise.resolve(),
58
+ query: (query) => Promise.resolve(selectRows(state.tables, query)),
59
+ exec: (statement) => Promise.resolve(execStatement(state.tables, statement)),
60
+ seed: (data) => Promise.resolve(seedTables(state.tables, data)),
61
+ snapshot: () => Promise.resolve(cloneTables(state.tables)),
62
+ restore: (snapshot) => restore(state, snapshot)
63
+ };
64
+ }
65
+ function restore(state, snapshot) {
66
+ state.tables = cloneTables(snapshot);
67
+ return Promise.resolve();
68
+ }
69
+ //#endregion
70
+ //#region src/clients/real-client.ts
71
+ /**
72
+ * The driver-backed `DbClient`. Not implemented in this build.
73
+ *
74
+ * @throws VennError `VN8090` from every method, so a host that binds it fails
75
+ * loudly instead of quietly answering with nothing.
76
+ */
77
+ function createRealDbClient() {
78
+ return {
79
+ connect: () => notImplemented(),
80
+ query: () => notImplemented(),
81
+ exec: () => notImplemented(),
82
+ seed: () => notImplemented(),
83
+ snapshot: () => notImplemented(),
84
+ restore: () => notImplemented()
85
+ };
86
+ }
87
+ function notImplemented() {
88
+ throw new VennError({
89
+ code: "VN8090",
90
+ message: "Db real client not implemented in this build"
91
+ });
92
+ }
93
+ //#endregion
94
+ //#region src/port/db-client.port.ts
95
+ /**
96
+ * The port every `db` verb reaches the database through.
97
+ *
98
+ * Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires
99
+ * the `net` capability, so a host without it is refused at load time rather
100
+ * than failing mid-run.
101
+ */
102
+ const DbClientPort = {
103
+ id: "venn.port.db-client",
104
+ version: 1,
105
+ requires: ["net"],
106
+ methods: [
107
+ "connect",
108
+ "query",
109
+ "exec",
110
+ "seed",
111
+ "snapshot",
112
+ "restore"
113
+ ]
114
+ };
115
+ //#endregion
116
+ //#region src/actions/connect.ts
117
+ /** `db.connect(url)`: open the connection the other `db` verbs then use. */
118
+ const connectAction = defineAction({
119
+ name: "connect",
120
+ doc: "Connect to a database by URL.",
121
+ args: [arg("url", t.string, "Where the database is.")],
122
+ result: t.string,
123
+ run: async (ctx, input) => {
124
+ const url = String(input.args[0] ?? "");
125
+ await ctx.port(DbClientPort).connect(url);
126
+ return url;
127
+ }
128
+ });
129
+ //#endregion
130
+ //#region src/types/row.ts
131
+ /** Zod schema for the nominal `Row` type the plugin contributes. */
132
+ const RowSchema = z.record(z.string(), z.unknown());
133
+ //#endregion
134
+ //#region src/types/type-defs.ts
135
+ /**
136
+ * The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.
137
+ *
138
+ * Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which
139
+ * `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.
140
+ */
141
+ const dbTypeDefs = {
142
+ /**
143
+ * One row. Which columns it has is the query's business, not the plugin's, so
144
+ * the type says only what is always true: names to values.
145
+ */
146
+ Row: t.map(t.dynamic),
147
+ /** Rows grouped by table name. What `db.seed` takes and `db.snapshot` gives back. */
148
+ Tables: t.map(t.list(t.ref("db.Row")))
149
+ };
150
+ /** `db.exec("TRUNCATE …", { rows })`: mutate the tables and answer with the affected count. */
151
+ const execAction = defineAction({
152
+ name: "exec",
153
+ doc: "Execute a mutating statement (TRUNCATE/DELETE/INSERT); returns the affected count.",
154
+ params: z.object({ rows: z.array(RowSchema).optional() }),
155
+ args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
156
+ result: t.number,
157
+ run: (ctx, input) => ctx.port(DbClientPort).exec({
158
+ sql: String(input.args[0] ?? ""),
159
+ rows: input.params.rows
160
+ })
161
+ });
162
+ /** `db.query("SELECT …", { where })`: run a SELECT and answer with the matching rows. */
163
+ const queryAction = defineAction({
164
+ name: "query",
165
+ doc: "Run a SELECT and return the matching rows.",
166
+ params: z.object({ where: RowSchema.optional() }),
167
+ args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
168
+ result: t.list(t.ref("db.Row")),
169
+ run: (ctx, input) => ctx.port(DbClientPort).query({
170
+ sql: String(input.args[0] ?? ""),
171
+ where: input.params.where
172
+ })
173
+ });
174
+ //#endregion
175
+ //#region src/actions/restore.ts
176
+ /** `db.restore(snapshot)`: put the tables back as `db.snapshot` found them. */
177
+ const restoreAction = defineAction({
178
+ name: "restore",
179
+ doc: "Restore a previously captured snapshot.",
180
+ args: [arg("snapshot", t.ref("db.Tables"), "What `db.snapshot` gave back.")],
181
+ result: t.void,
182
+ run: (ctx, input) => ctx.port(DbClientPort).restore(input.args[0])
183
+ });
184
+ //#endregion
185
+ //#region src/actions/seed.ts
186
+ /** `db.seed(data)`: load rows into the tables and answer with how many were loaded. */
187
+ const seedAction = defineAction({
188
+ name: "seed",
189
+ doc: "Load rows into the in-memory tables, grouped by table name.",
190
+ args: [arg("tables", t.ref("db.Tables"), "Rows to load, grouped by table name.")],
191
+ result: t.number,
192
+ run: (ctx, input) => ctx.port(DbClientPort).seed(readSeedData(input))
193
+ });
194
+ /**
195
+ * Read the tables from either call shape.
196
+ *
197
+ * `db.seed baseline` passes them positionally. Written inline, `db.seed { … }`
198
+ * puts them in the options map instead, and no signature parameter names those.
199
+ */
200
+ function readSeedData(input) {
201
+ const positional = input.args[0];
202
+ if (positional && typeof positional === "object") return positional;
203
+ return input.params ?? {};
204
+ }
205
+ //#endregion
206
+ //#region src/plugin.ts
207
+ /**
208
+ * The `db` plugin: connect, query, exec, seed, snapshot and restore.
209
+ *
210
+ * Every verb reaches the database through `DbClientPort`, so the host decides
211
+ * whether that is the fake or a real driver. Loading fails up front unless the
212
+ * host offers the `net` capability.
213
+ */
214
+ const dbPlugin = definePlugin({
215
+ name: "venn/db",
216
+ version: "0.0.0",
217
+ namespace: "db",
218
+ requires: ["net"],
219
+ actions: [
220
+ connectAction,
221
+ queryAction,
222
+ execAction,
223
+ seedAction,
224
+ defineAction({
225
+ name: "snapshot",
226
+ doc: "Capture the current in-memory state as a restorable snapshot.",
227
+ result: t.ref("db.Tables"),
228
+ run: (ctx) => ctx.port(DbClientPort).snapshot()
229
+ }),
230
+ restoreAction
231
+ ],
232
+ types: { Row: RowSchema },
233
+ typeDefs: dbTypeDefs
234
+ });
235
+ //#endregion
236
+ export { DbClientPort, RowSchema, createFakeDbClient, createRealDbClient, dbPlugin, dbPlugin as default, dbTypeDefs };
237
+
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/clients/query-engine.ts","../src/clients/fake-client.ts","../src/clients/real-client.ts","../src/port/db-client.port.ts","../src/actions/connect.ts","../src/types/row.ts","../src/types/type-defs.ts","../src/actions/exec.ts","../src/actions/query.ts","../src/actions/restore.ts","../src/actions/seed.ts","../src/actions/snapshot.ts","../src/actions/index.ts","../src/plugin.ts"],"sourcesContent":["import type { ExecArgs, QueryArgs, SeedData, TableMap } from \"../port/index.js\";\nimport type { Row } from \"../types/index.js\";\n\n// A deliberately small SQL reader for the fake: it recognises the table name and\n// little else. Enough to make a seeded test read like the real thing.\n\n/** Deep copy of the tables. A snapshot that aliased live state would follow later edits. */\nexport function cloneTables(tables: TableMap): TableMap {\n return structuredClone(tables);\n}\n\n/** A tiny SELECT: pick the table named after `FROM`, optionally filter by `where`. */\nexport function selectRows(tables: TableMap, query: QueryArgs): Row[] {\n const rows = tables[tableName(query.sql, /from\\s+([a-z_]\\w*)/i)] ?? [];\n const where = query.where;\n const matched = where ? rows.filter((row) => matchesWhere(row, where)) : rows;\n return matched.map((row) => ({ ...row }));\n}\n\n/** A tiny mutation: INSERT appends rows; TRUNCATE/DELETE clears. Returns the count. */\nexport function execStatement(tables: TableMap, statement: ExecArgs): number {\n const inserted = tableName(statement.sql, /insert\\s+into\\s+([a-z_]\\w*)/i);\n if (inserted) return insertRows(tables, inserted, statement.rows ?? []);\n return truncate(tables, tableName(statement.sql, /(?:truncate|delete\\s+from)\\s+([a-z_]\\w*)/i));\n}\n\n/** Append rows into named tables; returns how many rows were loaded. */\nexport function seedTables(tables: TableMap, data: SeedData): number {\n let count = 0;\n for (const [name, rows] of Object.entries(data)) {\n const copies = rows.map((row) => ({ ...row }));\n tables[name] = [...(tables[name] ?? []), ...copies];\n count += copies.length;\n }\n return count;\n}\n\nfunction insertRows(tables: TableMap, name: string, rows: Row[]): number {\n const copies = rows.map((row) => ({ ...row }));\n tables[name] = [...(tables[name] ?? []), ...copies];\n return copies.length;\n}\n\nfunction truncate(tables: TableMap, name: string): number {\n const removed = tables[name]?.length ?? 0;\n if (name) tables[name] = [];\n return removed;\n}\n\nfunction matchesWhere(row: Row, where: Row): boolean {\n return Object.entries(where).every(([key, value]) => row[key] === value);\n}\n\nfunction tableName(sql: string, pattern: RegExp): string {\n return pattern.exec(sql)?.[1] ?? \"\";\n}\n","import type { DbClient, DbSnapshot, SeedData, TableMap } from \"../port/index.js\";\nimport { cloneTables, execStatement, seedTables, selectRows } from \"./query-engine.js\";\n\ninterface DbState {\n tables: TableMap;\n}\n\n/**\n * The in-memory `DbClient`: deterministic tables, no connection, no I/O.\n *\n * @param args.tables Rows to start from. Copied, so the caller's object is never mutated.\n */\nexport function createFakeDbClient(args: { tables?: SeedData } = {}): DbClient {\n const state: DbState = { tables: cloneTables(args.tables ?? {}) };\n return {\n connect: () => Promise.resolve(),\n query: (query) => Promise.resolve(selectRows(state.tables, query)),\n exec: (statement) => Promise.resolve(execStatement(state.tables, statement)),\n seed: (data) => Promise.resolve(seedTables(state.tables, data)),\n snapshot: () => Promise.resolve(cloneTables(state.tables)),\n restore: (snapshot) => restore(state, snapshot),\n };\n}\n\nfunction restore(state: DbState, snapshot: DbSnapshot): Promise<void> {\n state.tables = cloneTables(snapshot);\n return Promise.resolve();\n}\n","import { VennError } from \"@venn-lang/contracts\";\nimport type { DbClient } from \"../port/index.js\";\n\n/**\n * The driver-backed `DbClient`. Not implemented in this build.\n *\n * @throws VennError `VN8090` from every method, so a host that binds it fails\n * loudly instead of quietly answering with nothing.\n */\nexport function createRealDbClient(): DbClient {\n return {\n connect: () => notImplemented(),\n query: () => notImplemented(),\n exec: () => notImplemented(),\n seed: () => notImplemented(),\n snapshot: () => notImplemented(),\n restore: () => notImplemented(),\n };\n}\n\nfunction notImplemented(): never {\n throw new VennError({\n code: \"VN8090\",\n message: \"Db real client not implemented in this build\",\n });\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { DbClient } from \"./db-client.types.js\";\n\n/**\n * The port every `db` verb reaches the database through.\n *\n * Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires\n * the `net` capability, so a host without it is refused at load time rather\n * than failing mid-run.\n */\nexport const DbClientPort: Port<DbClient> = {\n id: \"venn.port.db-client\",\n version: 1,\n requires: [\"net\"],\n methods: [\"connect\", \"query\", \"exec\", \"seed\", \"snapshot\", \"restore\"],\n};\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\n\n/** `db.connect(url)`: open the connection the other `db` verbs then use. */\nexport const connectAction: ActionDefinition = defineAction({\n name: \"connect\",\n doc: \"Connect to a database by URL.\",\n // It answers with the URL, not a handle: the connection lives behind the port,\n // and every other verb reaches it from there.\n args: [arg(\"url\", t.string, \"Where the database is.\")],\n result: t.string,\n run: async (ctx, input) => {\n const url = String(input.args[0] ?? \"\");\n await ctx.port(DbClientPort).connect(url);\n return url;\n },\n});\n","import { type ZodType, z } from \"@venn-lang/sdk\";\n\n/** The nominal `Row` type: an opaque record of column values. */\nexport type Row = Record<string, unknown>;\n\n/** Zod schema for the nominal `Row` type the plugin contributes. */\nexport const RowSchema: ZodType<Row> = z.record(z.string(), z.unknown());\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.\n *\n * Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which\n * `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.\n */\nexport const dbTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /**\n * One row. Which columns it has is the query's business, not the plugin's, so\n * the type says only what is always true: names to values.\n */\n Row: t.map(t.dynamic),\n /** Rows grouped by table name. What `db.seed` takes and `db.snapshot` gives back. */\n Tables: t.map(t.list(t.ref(\"db.Row\"))),\n};\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\nimport { RowSchema } from \"../types/index.js\";\n\nconst execParams = z.object({ rows: z.array(RowSchema).optional() });\n\n/** `db.exec(\"TRUNCATE …\", { rows })`: mutate the tables and answer with the affected count. */\nexport const execAction: ActionDefinition = defineAction({\n name: \"exec\",\n doc: \"Execute a mutating statement (TRUNCATE/DELETE/INSERT); returns the affected count.\",\n params: execParams,\n args: [arg(\"sql\", t.string, \"The statement to run. Placeholders go in the options.\")],\n result: t.number,\n run: (ctx, input) =>\n ctx.port(DbClientPort).exec({ sql: String(input.args[0] ?? \"\"), rows: input.params.rows }),\n});\n","import { type ActionDefinition, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\nimport { RowSchema } from \"../types/index.js\";\n\nconst queryParams = z.object({ where: RowSchema.optional() });\n\n/** `db.query(\"SELECT …\", { where })`: run a SELECT and answer with the matching rows. */\nexport const queryAction: ActionDefinition = defineAction({\n name: \"query\",\n doc: \"Run a SELECT and return the matching rows.\",\n params: queryParams,\n args: [arg(\"sql\", t.string, \"The statement to run. Placeholders go in the options.\")],\n result: t.list(t.ref(\"db.Row\")),\n run: (ctx, input) =>\n ctx.port(DbClientPort).query({ sql: String(input.args[0] ?? \"\"), where: input.params.where }),\n});\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort, type DbSnapshot } from \"../port/index.js\";\n\n/** `db.restore(snapshot)`: put the tables back as `db.snapshot` found them. */\nexport const restoreAction: ActionDefinition = defineAction({\n name: \"restore\",\n doc: \"Restore a previously captured snapshot.\",\n args: [arg(\"snapshot\", t.ref(\"db.Tables\"), \"What `db.snapshot` gave back.\")],\n result: t.void,\n run: (ctx, input) => ctx.port(DbClientPort).restore(input.args[0] as DbSnapshot),\n});\n","import { type ActionDefinition, type ActionInput, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort, type SeedData } from \"../port/index.js\";\n\n/** `db.seed(data)`: load rows into the tables and answer with how many were loaded. */\nexport const seedAction: ActionDefinition = defineAction({\n name: \"seed\",\n doc: \"Load rows into the in-memory tables, grouped by table name.\",\n args: [arg(\"tables\", t.ref(\"db.Tables\"), \"Rows to load, grouped by table name.\")],\n result: t.number,\n run: (ctx, input) => ctx.port(DbClientPort).seed(readSeedData(input)),\n});\n\n/**\n * Read the tables from either call shape.\n *\n * `db.seed baseline` passes them positionally. Written inline, `db.seed { … }`\n * puts them in the options map instead, and no signature parameter names those.\n */\nfunction readSeedData(input: ActionInput<unknown>): SeedData {\n const positional = input.args[0];\n if (positional && typeof positional === \"object\") return positional as SeedData;\n return (input.params ?? {}) as SeedData;\n}\n","import { type ActionDefinition, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { DbClientPort } from \"../port/index.js\";\n\n/** `db.snapshot()`: copy the current tables into a value `db.restore` accepts. */\nexport const snapshotAction: ActionDefinition = defineAction({\n name: \"snapshot\",\n doc: \"Capture the current in-memory state as a restorable snapshot.\",\n // `db.Tables`, not `DbSnapshot`: that alias lives in the port's TypeScript, and\n // a script can only name what the plugin publishes.\n result: t.ref(\"db.Tables\"),\n run: (ctx) => ctx.port(DbClientPort).snapshot(),\n});\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { connectAction } from \"./connect.js\";\nimport { execAction } from \"./exec.js\";\nimport { queryAction } from \"./query.js\";\nimport { restoreAction } from \"./restore.js\";\nimport { seedAction } from \"./seed.js\";\nimport { snapshotAction } from \"./snapshot.js\";\n\n/** The db namespace's verbs. Adding one is a single line here. */\nexport const dbActions: ActionDefinition[] = [\n connectAction,\n queryAction,\n execAction,\n seedAction,\n snapshotAction,\n restoreAction,\n];\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { dbActions } from \"./actions/index.js\";\nimport { dbTypeDefs, RowSchema } from \"./types/index.js\";\n\n/**\n * The `db` plugin: connect, query, exec, seed, snapshot and restore.\n *\n * Every verb reaches the database through `DbClientPort`, so the host decides\n * whether that is the fake or a real driver. Loading fails up front unless the\n * host offers the `net` capability.\n */\nexport const dbPlugin: PluginDefinition = definePlugin({\n name: \"venn/db\",\n version: \"0.0.0\",\n namespace: \"db\",\n requires: [\"net\"],\n actions: dbActions,\n types: { Row: RowSchema },\n typeDefs: dbTypeDefs,\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/db",
3
+ "version": "0.1.0",
4
+ "description": "The db namespace: seed tables, run statements and snapshot state, all through the DbClient port.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "db"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-db#readme",
12
+ "bugs": "https://github.com/venn-lang/venn/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/venn-lang/venn.git",
16
+ "directory": "packages/std-db"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Vinicius Borges",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "development": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "!src/**/*.suite.ts"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@venn-lang/contracts": "0.1.0",
41
+ "@venn-lang/sdk": "0.1.0",
42
+ "@venn-lang/types": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.22.14",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { DbClientPort } from "../port/index.js";
4
+
5
+ /** `db.connect(url)`: open the connection the other `db` verbs then use. */
6
+ export const connectAction: ActionDefinition = defineAction({
7
+ name: "connect",
8
+ doc: "Connect to a database by URL.",
9
+ // It answers with the URL, not a handle: the connection lives behind the port,
10
+ // and every other verb reaches it from there.
11
+ args: [arg("url", t.string, "Where the database is.")],
12
+ result: t.string,
13
+ run: async (ctx, input) => {
14
+ const url = String(input.args[0] ?? "");
15
+ await ctx.port(DbClientPort).connect(url);
16
+ return url;
17
+ },
18
+ });
@@ -0,0 +1,17 @@
1
+ import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { DbClientPort } from "../port/index.js";
4
+ import { RowSchema } from "../types/index.js";
5
+
6
+ const execParams = z.object({ rows: z.array(RowSchema).optional() });
7
+
8
+ /** `db.exec("TRUNCATE …", { rows })`: mutate the tables and answer with the affected count. */
9
+ export const execAction: ActionDefinition = defineAction({
10
+ name: "exec",
11
+ doc: "Execute a mutating statement (TRUNCATE/DELETE/INSERT); returns the affected count.",
12
+ params: execParams,
13
+ args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
14
+ result: t.number,
15
+ run: (ctx, input) =>
16
+ ctx.port(DbClientPort).exec({ sql: String(input.args[0] ?? ""), rows: input.params.rows }),
17
+ });
@@ -0,0 +1,17 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { connectAction } from "./connect.js";
3
+ import { execAction } from "./exec.js";
4
+ import { queryAction } from "./query.js";
5
+ import { restoreAction } from "./restore.js";
6
+ import { seedAction } from "./seed.js";
7
+ import { snapshotAction } from "./snapshot.js";
8
+
9
+ /** The db namespace's verbs. Adding one is a single line here. */
10
+ export const dbActions: ActionDefinition[] = [
11
+ connectAction,
12
+ queryAction,
13
+ execAction,
14
+ seedAction,
15
+ snapshotAction,
16
+ restoreAction,
17
+ ];
@@ -0,0 +1,17 @@
1
+ import { type ActionDefinition, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { DbClientPort } from "../port/index.js";
4
+ import { RowSchema } from "../types/index.js";
5
+
6
+ const queryParams = z.object({ where: RowSchema.optional() });
7
+
8
+ /** `db.query("SELECT …", { where })`: run a SELECT and answer with the matching rows. */
9
+ export const queryAction: ActionDefinition = defineAction({
10
+ name: "query",
11
+ doc: "Run a SELECT and return the matching rows.",
12
+ params: queryParams,
13
+ args: [arg("sql", t.string, "The statement to run. Placeholders go in the options.")],
14
+ result: t.list(t.ref("db.Row")),
15
+ run: (ctx, input) =>
16
+ ctx.port(DbClientPort).query({ sql: String(input.args[0] ?? ""), where: input.params.where }),
17
+ });
@@ -0,0 +1,12 @@
1
+ import { type ActionDefinition, arg, defineAction } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { DbClientPort, type DbSnapshot } from "../port/index.js";
4
+
5
+ /** `db.restore(snapshot)`: put the tables back as `db.snapshot` found them. */
6
+ export const restoreAction: ActionDefinition = defineAction({
7
+ name: "restore",
8
+ doc: "Restore a previously captured snapshot.",
9
+ args: [arg("snapshot", t.ref("db.Tables"), "What `db.snapshot` gave back.")],
10
+ result: t.void,
11
+ run: (ctx, input) => ctx.port(DbClientPort).restore(input.args[0] as DbSnapshot),
12
+ });
@@ -0,0 +1,24 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { DbClientPort, type SeedData } from "../port/index.js";
4
+
5
+ /** `db.seed(data)`: load rows into the tables and answer with how many were loaded. */
6
+ export const seedAction: ActionDefinition = defineAction({
7
+ name: "seed",
8
+ doc: "Load rows into the in-memory tables, grouped by table name.",
9
+ args: [arg("tables", t.ref("db.Tables"), "Rows to load, grouped by table name.")],
10
+ result: t.number,
11
+ run: (ctx, input) => ctx.port(DbClientPort).seed(readSeedData(input)),
12
+ });
13
+
14
+ /**
15
+ * Read the tables from either call shape.
16
+ *
17
+ * `db.seed baseline` passes them positionally. Written inline, `db.seed { … }`
18
+ * puts them in the options map instead, and no signature parameter names those.
19
+ */
20
+ function readSeedData(input: ActionInput<unknown>): SeedData {
21
+ const positional = input.args[0];
22
+ if (positional && typeof positional === "object") return positional as SeedData;
23
+ return (input.params ?? {}) as SeedData;
24
+ }
@@ -0,0 +1,13 @@
1
+ import { type ActionDefinition, defineAction } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { DbClientPort } from "../port/index.js";
4
+
5
+ /** `db.snapshot()`: copy the current tables into a value `db.restore` accepts. */
6
+ export const snapshotAction: ActionDefinition = defineAction({
7
+ name: "snapshot",
8
+ doc: "Capture the current in-memory state as a restorable snapshot.",
9
+ // `db.Tables`, not `DbSnapshot`: that alias lives in the port's TypeScript, and
10
+ // a script can only name what the plugin publishes.
11
+ result: t.ref("db.Tables"),
12
+ run: (ctx) => ctx.port(DbClientPort).snapshot(),
13
+ });
@@ -0,0 +1,28 @@
1
+ import type { DbClient, DbSnapshot, SeedData, TableMap } from "../port/index.js";
2
+ import { cloneTables, execStatement, seedTables, selectRows } from "./query-engine.js";
3
+
4
+ interface DbState {
5
+ tables: TableMap;
6
+ }
7
+
8
+ /**
9
+ * The in-memory `DbClient`: deterministic tables, no connection, no I/O.
10
+ *
11
+ * @param args.tables Rows to start from. Copied, so the caller's object is never mutated.
12
+ */
13
+ export function createFakeDbClient(args: { tables?: SeedData } = {}): DbClient {
14
+ const state: DbState = { tables: cloneTables(args.tables ?? {}) };
15
+ return {
16
+ connect: () => Promise.resolve(),
17
+ query: (query) => Promise.resolve(selectRows(state.tables, query)),
18
+ exec: (statement) => Promise.resolve(execStatement(state.tables, statement)),
19
+ seed: (data) => Promise.resolve(seedTables(state.tables, data)),
20
+ snapshot: () => Promise.resolve(cloneTables(state.tables)),
21
+ restore: (snapshot) => restore(state, snapshot),
22
+ };
23
+ }
24
+
25
+ function restore(state: DbState, snapshot: DbSnapshot): Promise<void> {
26
+ state.tables = cloneTables(snapshot);
27
+ return Promise.resolve();
28
+ }
@@ -0,0 +1,2 @@
1
+ export { createFakeDbClient } from "./fake-client.js";
2
+ export { createRealDbClient } from "./real-client.js";
@@ -0,0 +1,56 @@
1
+ import type { ExecArgs, QueryArgs, SeedData, TableMap } from "../port/index.js";
2
+ import type { Row } from "../types/index.js";
3
+
4
+ // A deliberately small SQL reader for the fake: it recognises the table name and
5
+ // little else. Enough to make a seeded test read like the real thing.
6
+
7
+ /** Deep copy of the tables. A snapshot that aliased live state would follow later edits. */
8
+ export function cloneTables(tables: TableMap): TableMap {
9
+ return structuredClone(tables);
10
+ }
11
+
12
+ /** A tiny SELECT: pick the table named after `FROM`, optionally filter by `where`. */
13
+ export function selectRows(tables: TableMap, query: QueryArgs): Row[] {
14
+ const rows = tables[tableName(query.sql, /from\s+([a-z_]\w*)/i)] ?? [];
15
+ const where = query.where;
16
+ const matched = where ? rows.filter((row) => matchesWhere(row, where)) : rows;
17
+ return matched.map((row) => ({ ...row }));
18
+ }
19
+
20
+ /** A tiny mutation: INSERT appends rows; TRUNCATE/DELETE clears. Returns the count. */
21
+ export function execStatement(tables: TableMap, statement: ExecArgs): number {
22
+ const inserted = tableName(statement.sql, /insert\s+into\s+([a-z_]\w*)/i);
23
+ if (inserted) return insertRows(tables, inserted, statement.rows ?? []);
24
+ return truncate(tables, tableName(statement.sql, /(?:truncate|delete\s+from)\s+([a-z_]\w*)/i));
25
+ }
26
+
27
+ /** Append rows into named tables; returns how many rows were loaded. */
28
+ export function seedTables(tables: TableMap, data: SeedData): number {
29
+ let count = 0;
30
+ for (const [name, rows] of Object.entries(data)) {
31
+ const copies = rows.map((row) => ({ ...row }));
32
+ tables[name] = [...(tables[name] ?? []), ...copies];
33
+ count += copies.length;
34
+ }
35
+ return count;
36
+ }
37
+
38
+ function insertRows(tables: TableMap, name: string, rows: Row[]): number {
39
+ const copies = rows.map((row) => ({ ...row }));
40
+ tables[name] = [...(tables[name] ?? []), ...copies];
41
+ return copies.length;
42
+ }
43
+
44
+ function truncate(tables: TableMap, name: string): number {
45
+ const removed = tables[name]?.length ?? 0;
46
+ if (name) tables[name] = [];
47
+ return removed;
48
+ }
49
+
50
+ function matchesWhere(row: Row, where: Row): boolean {
51
+ return Object.entries(where).every(([key, value]) => row[key] === value);
52
+ }
53
+
54
+ function tableName(sql: string, pattern: RegExp): string {
55
+ return pattern.exec(sql)?.[1] ?? "";
56
+ }
@@ -0,0 +1,26 @@
1
+ import { VennError } from "@venn-lang/contracts";
2
+ import type { DbClient } from "../port/index.js";
3
+
4
+ /**
5
+ * The driver-backed `DbClient`. Not implemented in this build.
6
+ *
7
+ * @throws VennError `VN8090` from every method, so a host that binds it fails
8
+ * loudly instead of quietly answering with nothing.
9
+ */
10
+ export function createRealDbClient(): DbClient {
11
+ return {
12
+ connect: () => notImplemented(),
13
+ query: () => notImplemented(),
14
+ exec: () => notImplemented(),
15
+ seed: () => notImplemented(),
16
+ snapshot: () => notImplemented(),
17
+ restore: () => notImplemented(),
18
+ };
19
+ }
20
+
21
+ function notImplemented(): never {
22
+ throw new VennError({
23
+ code: "VN8090",
24
+ message: "Db real client not implemented in this build",
25
+ });
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ // @venn-lang/db: table verbs and a nominal Row type, riding the DbClient port so the
2
+ // database is injected at the host boundary rather than baked into the plugin.
3
+
4
+ export * from "./clients/index.js";
5
+ export { dbPlugin, dbPlugin as default } from "./plugin.js";
6
+ export * from "./port/index.js";
7
+ export * from "./types/index.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { dbActions } from "./actions/index.js";
3
+ import { dbTypeDefs, RowSchema } from "./types/index.js";
4
+
5
+ /**
6
+ * The `db` plugin: connect, query, exec, seed, snapshot and restore.
7
+ *
8
+ * Every verb reaches the database through `DbClientPort`, so the host decides
9
+ * whether that is the fake or a real driver. Loading fails up front unless the
10
+ * host offers the `net` capability.
11
+ */
12
+ export const dbPlugin: PluginDefinition = definePlugin({
13
+ name: "venn/db",
14
+ version: "0.0.0",
15
+ namespace: "db",
16
+ requires: ["net"],
17
+ actions: dbActions,
18
+ types: { Row: RowSchema },
19
+ typeDefs: dbTypeDefs,
20
+ });
@@ -0,0 +1,16 @@
1
+ import type { Port } from "@venn-lang/contracts";
2
+ import type { DbClient } from "./db-client.types.js";
3
+
4
+ /**
5
+ * The port every `db` verb reaches the database through.
6
+ *
7
+ * Bound by the host to `createFakeDbClient` or `createRealDbClient`. Requires
8
+ * the `net` capability, so a host without it is refused at load time rather
9
+ * than failing mid-run.
10
+ */
11
+ export const DbClientPort: Port<DbClient> = {
12
+ id: "venn.port.db-client",
13
+ version: 1,
14
+ requires: ["net"],
15
+ methods: ["connect", "query", "exec", "seed", "snapshot", "restore"],
16
+ };
@@ -0,0 +1,35 @@
1
+ import type { Row } from "../types/index.js";
2
+
3
+ /** Arguments for {@link DbClient.query}: a SQL string plus an optional filter. */
4
+ export interface QueryArgs {
5
+ sql: string;
6
+ where?: Row;
7
+ }
8
+
9
+ /** Arguments for {@link DbClient.exec}: a mutating statement plus optional rows. */
10
+ export interface ExecArgs {
11
+ sql: string;
12
+ rows?: Row[];
13
+ }
14
+
15
+ /** Rows keyed by table name. The shape seeds and snapshots share. */
16
+ export type TableMap = Record<string, Row[]>;
17
+
18
+ /** Rows to load, keyed by table name. */
19
+ export type SeedData = TableMap;
20
+
21
+ /** A captured copy of every table. Detached from live state, so it can be restored later. */
22
+ export type DbSnapshot = TableMap;
23
+
24
+ /**
25
+ * The contract a database is reached through. Actions call it via
26
+ * `ctx.port(DbClientPort)` and never touch a driver directly.
27
+ */
28
+ export interface DbClient {
29
+ connect(url: string): Promise<void>;
30
+ query(args: QueryArgs): Promise<Row[]>;
31
+ exec(args: ExecArgs): Promise<number>;
32
+ seed(data: SeedData): Promise<number>;
33
+ snapshot(): Promise<DbSnapshot>;
34
+ restore(snapshot: DbSnapshot): Promise<void>;
35
+ }
@@ -0,0 +1,9 @@
1
+ export { DbClientPort } from "./db-client.port.js";
2
+ export type {
3
+ DbClient,
4
+ DbSnapshot,
5
+ ExecArgs,
6
+ QueryArgs,
7
+ SeedData,
8
+ TableMap,
9
+ } from "./db-client.types.js";
@@ -0,0 +1,3 @@
1
+ export type { Row } from "./row.js";
2
+ export { RowSchema } from "./row.js";
3
+ export { dbTypeDefs } from "./type-defs.js";
@@ -0,0 +1,7 @@
1
+ import { type ZodType, z } from "@venn-lang/sdk";
2
+
3
+ /** The nominal `Row` type: an opaque record of column values. */
4
+ export type Row = Record<string, unknown>;
5
+
6
+ /** Zod schema for the nominal `Row` type the plugin contributes. */
7
+ export const RowSchema: ZodType<Row> = z.record(z.string(), z.unknown());
@@ -0,0 +1,17 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The named types `@venn-lang/db` publishes to scripts, keyed by their bare name.
5
+ *
6
+ * Hand-mirrored from `port/db-client.types.ts` (`Row` and `TableMap`, which
7
+ * `SeedData` and `DbSnapshot` both alias): the two must agree, so change them together.
8
+ */
9
+ export const dbTypeDefs: Readonly<Record<string, TypeSpec>> = {
10
+ /**
11
+ * One row. Which columns it has is the query's business, not the plugin's, so
12
+ * the type says only what is always true: names to values.
13
+ */
14
+ Row: t.map(t.dynamic),
15
+ /** Rows grouped by table name. What `db.seed` takes and `db.snapshot` gives back. */
16
+ Tables: t.map(t.list(t.ref("db.Row"))),
17
+ };