quiverdb 0.9.14 → 0.9.16

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
@@ -130,6 +130,10 @@ Parameters are passed as an array of `number | string | null`.
130
130
  - `commit()` -- Commit current transaction
131
131
  - `rollback()` -- Rollback current transaction
132
132
  - `inTransaction()` -- Check if transaction is active
133
+ - `beginDryRun()` -- Begin a dry run: a transaction `endDryRun()` always rolls back. While it is
134
+ active, `beginTransaction`/`commit`/`rollback` are absorbed (no-ops)
135
+ - `endDryRun()` -- End the active dry run, rolling back everything it covered
136
+ - `inDryRun()` -- Check if a dry run is active
133
137
 
134
138
  ### CSV
135
139
 
@@ -152,7 +156,8 @@ Parameters are passed as an array of `number | string | null`.
152
156
  ### Lua
153
157
 
154
158
  - `LuaRunner(db)` -- Create Lua script runner with database access
155
- - `run(script)` -- Execute a Lua script
159
+ - `run(script)` -- Execute a Lua script; returns its return value as a JSON string, or `""` if it
160
+ returned nothing
156
161
  - `close()` -- Close the Lua runner
157
162
 
158
163
  ## Types
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/create.ts CHANGED
@@ -22,7 +22,7 @@ function setElementArray(
22
22
  const nameBuf = toCString(name);
23
23
 
24
24
  if (values.length === 0) {
25
- check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, null, 0));
25
+ check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, null, 0, null));
26
26
  return;
27
27
  }
28
28
 
@@ -30,7 +30,7 @@ function setElementArray(
30
30
 
31
31
  if (typeof first === "bigint") {
32
32
  const arr = allocNativeInt64(values as bigint[]);
33
- check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length));
33
+ check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
34
34
  return;
35
35
  }
36
36
 
@@ -38,17 +38,17 @@ function setElementArray(
38
38
  const allIntegers = (values as number[]).every((v) => Number.isInteger(v));
39
39
  if (allIntegers) {
40
40
  const arr = allocNativeInt64(values as number[]);
41
- check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length));
41
+ check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
42
42
  } else {
43
43
  const arr = allocNativeFloat64(values as number[]);
44
- check(lib.quiver_element_set_array_float(elemPtr, nameBuf.buf, arr.buf, values.length));
44
+ check(lib.quiver_element_set_array_float(elemPtr, nameBuf.buf, arr.buf, values.length, null));
45
45
  }
46
46
  return;
47
47
  }
48
48
 
49
49
  if (typeof first === "string") {
50
50
  const { table, keepalive: _keepalive } = allocNativeStringArray(values as string[]);
51
- check(lib.quiver_element_set_array_string(elemPtr, nameBuf.buf, table.buf, values.length));
51
+ check(lib.quiver_element_set_array_string(elemPtr, nameBuf.buf, table.buf, values.length, null));
52
52
  return;
53
53
  }
54
54
 
package/src/database.ts CHANGED
@@ -126,6 +126,9 @@ export class Database {
126
126
  declare commit: () => void;
127
127
  declare rollback: () => void;
128
128
  declare inTransaction: () => boolean;
129
+ declare beginDryRun: () => void;
130
+ declare endDryRun: () => void;
131
+ declare inDryRun: () => boolean;
129
132
 
130
133
  // --- Metadata (implemented in metadata.ts) ---
131
134
  declare getScalarMetadata: (collection: string, attribute: string) => ScalarMetadata;
package/src/loader.ts CHANGED
@@ -50,9 +50,9 @@ const elementSymbols = {
50
50
  quiver_element_set_float: { args: [P, BUF, F64], returns: I32 },
51
51
  quiver_element_set_string: { args: [P, BUF, BUF], returns: I32 },
52
52
  quiver_element_set_null: { args: [P, BUF], returns: I32 },
53
- quiver_element_set_array_integer: { args: [P, BUF, P, I32], returns: I32 },
54
- quiver_element_set_array_float: { args: [P, BUF, P, I32], returns: I32 },
55
- quiver_element_set_array_string: { args: [P, BUF, P, I32], returns: I32 },
53
+ quiver_element_set_array_integer: { args: [P, BUF, P, I32, P], returns: I32 },
54
+ quiver_element_set_array_float: { args: [P, BUF, P, I32, P], returns: I32 },
55
+ quiver_element_set_array_string: { args: [P, BUF, P, I32, P], returns: I32 },
56
56
  } as const;
57
57
 
58
58
  const crudSymbols = {
@@ -97,6 +97,9 @@ const transactionSymbols = {
97
97
  quiver_database_commit: { args: [P], returns: I32 },
98
98
  quiver_database_rollback: { args: [P], returns: I32 },
99
99
  quiver_database_in_transaction: { args: [P, P], returns: I32 },
100
+ quiver_database_begin_dry_run: { args: [P], returns: I32 },
101
+ quiver_database_end_dry_run: { args: [P], returns: I32 },
102
+ quiver_database_in_dry_run: { args: [P, P], returns: I32 },
100
103
  } as const;
101
104
 
102
105
  const metadataSymbols = {
@@ -160,7 +163,8 @@ const freeSymbols = {
160
163
  const luaSymbols = {
161
164
  quiver_lua_runner_new: { args: [P, P], returns: I32 },
162
165
  quiver_lua_runner_free: { args: [P], returns: I32 },
163
- quiver_lua_runner_run: { args: [P, BUF], returns: I32 },
166
+ quiver_lua_runner_run: { args: [P, BUF, BUF], returns: I32 },
167
+ quiver_lua_runner_free_string: { args: [P], returns: I32 },
164
168
  } as const;
165
169
 
166
170
  // Combined symbol map for dlopen.
package/src/lua-api.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  // Agent-facing reference for the Lua `db` API available inside run_lua scripts.
2
2
  //
3
3
  // Authority: `src/lua_runner.cpp` `bind_database()` (this repo) — extracted by hand, NOT imported.
4
- // The shipped quiverdb native binding is the runtime truth; this is docs. Whenever the C++ Lua
5
- // binding changes, re-diff every signature below against `bind_database()` (names, arg order, arg
6
- // types, return shapes).
4
+ // The shipped quiverdb native binding is the runtime truth; this is docs.
5
+ //
6
+ // SYNC: `test/lua-api-sync.test.ts` derives the bound surface from `src/lua_runner.cpp` and checks
7
+ // it AUTOMATICALLY — every `db:`/`quiver.*` name is documented, no documented name has been
8
+ // removed, and the stdlib sentence matches `open_libraries` exactly. What it CANNOT check, and you
9
+ // must still re-diff by hand when the binding changes: arg order, arity, arg types, return shapes,
10
+ // and whether the prose is semantically true.
7
11
  //
8
12
  // NOTE: the binary/expression subsystems are bound in the native binding and documented below.
9
13
  // File-touching operations (db:open_file, db:bin_to_csv, db:csv_to_bin, expr:save) are sandboxed
10
14
  // to the database file's directory; the pure-metadata builders stay under the quiver.* global.
11
15
  //
12
- // FORMAT CONVENTION: every db: method should appear at least once as the literal token
13
- // `db:<snake_case_name>` so coverage is greppable.
16
+ // FORMAT CONVENTION: every db: method appears at least once as the literal token
17
+ // `db:<snake_case_name>`, and every quiver.* function as `quiver.<name>`, so coverage is greppable
18
+ // (the sync test relies on this).
14
19
  export const LUA_DB_API_REFERENCE = `
15
20
  # Quiver Lua API Reference
16
21
 
@@ -66,19 +71,51 @@ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Py
66
71
  - **Errors abort the script.** Any error thrown by a \`db:\` call stops the script and surfaces as
67
72
  \`Failed to run Lua script: <message>\`. Validation failures roll back whatever the current
68
73
  transaction covered.
69
- - **Standard library.** Only the \`base\`, \`string\`, and \`table\` libraries are loaded — there is NO
70
- \`math\` (use \`//\` for integer division), and no \`os\`, \`io\`, or \`require\`; \`dofile\` and
71
- \`loadfile\` are removed (string-form \`load\` is available).
74
+ - **Standard library.** Loaded standard libraries: base, string, table, math, coroutine, utf8.
75
+ That is the pure-computation set — there is no \`os\`, \`io\`, \`debug\`, or \`package\`/\`require\`,
76
+ and \`dofile\`/\`loadfile\` are removed (string-form \`load\` stays available). Integer division is
77
+ the Lua 5.4 \`//\` operator — a language operator, unrelated to \`math\`.
72
78
  - **Filesystem sandbox.** Every file-touching operation (\`db:export_csv\`, \`db:import_csv\`,
73
79
  \`db:open_file\`, \`db:bin_to_csv\`, \`db:csv_to_bin\`, \`expr:save\`) resolves relative paths against
74
80
  the directory containing the database file and rejects anything outside it (subdirectories are
75
81
  fine; \`..\` escapes and outside absolute paths throw \`Cannot <op>: path '...' escapes the
76
82
  database directory ...\`). On an in-memory database these operations throw
77
83
  \`Cannot <op>: database is in-memory, file operations are unavailable\`.
78
- - **Output.** Scripts cannot return values to the tool — use \`print()\` for anything you need to
79
- see (it is captured). Arrays are 1-indexed (iterate with \`ipairs\`); reading a NULL yields \`nil\`,
80
- writing \`nil\` stores NULL where NULL is accepted (query params, ts rows, file columns — but NOT
81
- element scalar attributes; see CRUD).
84
+ - **Output.** A script can \`return\` one value and the host receives it as JSON — prefer this over
85
+ \`print()\` when you need structured data back (\`print()\` still works and is captured). Only the
86
+ **first** returned value is encoded. Arrays are 1-indexed (iterate with \`ipairs\`); reading a NULL
87
+ yields \`nil\`, writing \`nil\` stores NULL where NULL is accepted (query params, ts rows, file
88
+ columns — but NOT element scalar attributes; see CRUD).
89
+
90
+ \`\`\`lua
91
+ return { ids = db:read_element_ids("Collection"), total = 3 }
92
+ -- host receives: {"ids":[1,2,3],"total":3}
93
+ \`\`\`
94
+
95
+ | Returned | JSON |
96
+ | -------------------------- | ------------------------------------------------------- |
97
+ | nothing | \`""\` (empty — distinct from \`nil\`) |
98
+ | \`nil\` | \`null\` |
99
+ | integer / float / string | the value; a float keeps its shortest round-trip form |
100
+ | boolean | \`true\` / \`false\` |
101
+ | NaN, \`math.huge\` | \`null\` (JSON has no NaN/Infinity) |
102
+ | table keyed \`1..n\` | array — an empty table \`{}\` encodes as \`[]\` |
103
+ | any other table | object, keys sorted; integer keys stringify |
104
+
105
+ **A table with holes is an object, not an array.** A bulk read of a nullable column returns
106
+ \`nil\` holes (see Reading), so \`return db:read_scalar_integers(c, a)\` encodes as
107
+ \`{"1":10,"3":30}\` — not \`[10,null,30]\` — and the keys sort as text (\`"1","11","2"\`). When the host
108
+ needs positional data, return the ids alongside and fill the holes yourself:
109
+ \`local v = db:read_scalar_integers(c, a); local out = {}; for i in ipairs(ids) do out[i] = v[i] or false end\`.
110
+
111
+ Returning a function, a coroutine, or a userdata (including \`db\` itself) raises
112
+ \`Cannot run: script returned an unsupported Lua type\`; nesting deeper than 32 levels raises
113
+ \`Cannot run: script return value nests deeper than 32 levels\` (this is also what stops a
114
+ self-referencing table); a result over 64 MB raises
115
+ \`Cannot run: script return value exceeds ... bytes of JSON\`; a string holding bytes that are not
116
+ valid UTF-8 raises \`Cannot run: script return value contains a string that is not valid UTF-8\`;
117
+ and a table where an integer key and a string key spell the same thing (\`{[1] = 'a', ['1'] = 'b'}\`)
118
+ raises \`Cannot run: script returned a table with duplicate key '1'\`.
82
119
  - **Embedding harness may restrict further.** The library itself allows transactions and the
83
120
  (sandboxed) CSV/file operations below. A host that runs your script (e.g. a hosted \`run_lua\`
84
121
  tool) may disable some of them and report \`disabled in the run_lua sandbox\` — that limit comes
@@ -121,14 +158,54 @@ db:transaction(function(db)
121
158
  end) -- both writes commit together; if either throws, both roll back
122
159
  \`\`\`
123
160
 
124
- **Caveat:** if the host already runs your script inside a transaction, an explicit
125
- \`db:begin_transaction()\` will error (\`cannot start a transaction within a transaction\`) and a
126
- mid-script \`db:commit()\` would prematurely end the host's transaction. When unsure whether a
127
- transaction is already open, check \`db:in_transaction()\` first, or just issue writes directly —
161
+ **Caveat:** if the host already runs your script inside a plain transaction (not a dry run), an
162
+ explicit \`db:begin_transaction()\` will error (\`cannot start a transaction within a transaction\`)
163
+ and a mid-script \`db:commit()\` would prematurely end the host's transaction. When unsure whether
164
+ a transaction is already open, check \`db:in_transaction()\` first, or just issue writes directly —
128
165
  each \`db:\` write is durable on its own.
129
166
 
130
167
  ---
131
168
 
169
+ ## Dry runs
170
+
171
+ A dry run executes writes and then throws them away. Use it to check that a sequence works — that
172
+ the collections exist, the types match, the foreign keys resolve — before committing to it.
173
+
174
+ \`\`\`lua
175
+ db:dry_run(fn) -- run fn(db), roll everything back, return fn's result
176
+ db:begin_dry_run() -- start one explicitly
177
+ db:end_dry_run() -- end it, rolling back everything it covered
178
+ db:in_dry_run() -- boolean: is a dry run currently active?
179
+ \`\`\`
180
+
181
+ \`\`\`lua
182
+ local preview = db:dry_run(function(db)
183
+ db:create_element("Collection", { label = "Item 1", some_integer = 42 })
184
+ return db:read_element_ids("Collection") -- reads see the uncommitted writes
185
+ end)
186
+ -- nothing was kept; preview holds what the reads saw
187
+ \`\`\`
188
+
189
+ Rules worth knowing:
190
+
191
+ - **Nested transaction control is absorbed.** Inside a dry run, \`db:begin_transaction\`,
192
+ \`db:commit\` and \`db:rollback\` become no-ops, so the \`db:transaction\` pattern above composes
193
+ instead of erroring — \`db:transaction(fn)\` still runs \`fn\` and still performs its writes, only
194
+ its BEGIN/COMMIT are absorbed. The flip side: a nested \`db:rollback\` does **not** partially
195
+ undo — everything is undone when the dry run ends, whatever the nested calls asked for.
196
+ - \`db:in_transaction()\` still reports \`true\` during a dry run: a real transaction is open.
197
+ - \`db:import_csv\` cannot run inside a dry run (it toggles a pragma that is a no-op mid-transaction)
198
+ and throws \`Cannot import_csv: transaction already active\`.
199
+ - **Dry runs do not nest.** The host may have already opened one around your whole script, in which
200
+ case both \`db:begin_dry_run()\` and \`db:dry_run(fn)\` (which calls it internally) error with
201
+ \`Cannot begin_dry_run: dry run already active\` — check \`db:in_dry_run()\` first and skip the
202
+ wrapper when one is already active. Do **not** call \`db:end_dry_run()\` to get around it: that
203
+ ends the host's dry run, and everything you write afterwards is committed for real.
204
+ - For a rough sense of how much a run touched, \`db:query_integer("SELECT total_changes()")\` gives
205
+ the number of rows inserted, updated or deleted on this connection.
206
+
207
+ ---
208
+
132
209
  ## CRUD
133
210
 
134
211
  \`\`\`lua
@@ -164,8 +241,9 @@ Notes:
164
241
  type can't be inferred from an empty array), so it is silently dropped.
165
242
  - **No \`nil\` scalar attributes.** In Lua a key set to \`nil\` is dropped from the table, so
166
243
  \`{ x = nil }\` is identical to \`{}\`; an update/create table that ends up with no attributes
167
- **throws** (\`element must have at least one attribute\`). To leave a column unchanged, omit the
168
- key — you cannot set a scalar to NULL via the element table. (\`nil\` → NULL is only accepted by
244
+ **throws** (\`...must have at least one scalar attribute\` on create, \`...at least one attribute
245
+ to update\` on update). To leave a column unchanged, omit the key — you cannot set a scalar to
246
+ NULL via the element table. (\`nil\` → NULL is only accepted by
169
247
  \`upsert_time_series_row\` and \`update_time_series_files\`.)
170
248
 
171
249
  ---
@@ -489,7 +567,8 @@ db:csv_to_bin(path)
489
567
 
490
568
  local e = (quiver.expression(r) + 10.0) * 2.0 -- files auto-wrap; scalars either side
491
569
  e = quiver.abs(e); e = quiver.sqrt(e) -- also quiver.log / quiver.exp
492
- local cond_e = quiver.gt(e, 3.0) -- gt/lt/gte/lte/eq/neq -> 1.0/0.0 (NaN -> NaN)
570
+ local cond_e = quiver.gt(e, 3.0) -- also quiver.lt/quiver.gte/quiver.lte/quiver.eq/quiver.neq
571
+ -- all -> 1.0/0.0 per element (NaN operand -> NaN)
493
572
  cond_e = cond_e & ~quiver.lt(e, 1.0) -- boolean logic via & | ~ operators (and/or/not are keywords)
494
573
  e = quiver.ifelse(cond_e, then_e, else_e) -- build cond_e with comparison + logical operators
495
574
  e = e:aggregate("stage", "sum") -- sum/mean/min/max/percentile
package/src/lua-runner.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Database } from "./database.ts";
2
2
  import { check, QuiverError } from "./errors.ts";
3
- import { allocPtrOut, readPtrOut, toCString } from "./ffi-helpers.ts";
3
+ import { allocPtrOut, decodeStringFromBuf, readPtrOut, toCString } from "./ffi-helpers.ts";
4
4
  import type { NativePointer } from "./loader.ts";
5
5
  import { getSymbols } from "./loader.ts";
6
6
 
@@ -15,11 +15,21 @@ export class LuaRunner {
15
15
  this._ptr = readPtrOut(outRunner);
16
16
  }
17
17
 
18
- run(script: string): void {
18
+ /**
19
+ * Runs a Lua script and returns its return value encoded as JSON, or "" if it returned nothing.
20
+ *
21
+ * To execute a script without keeping its writes, wrap the call in
22
+ * `db.beginDryRun()` / `db.endDryRun()`.
23
+ */
24
+ run(script: string): string {
19
25
  this.ensureOpen();
20
26
  const lib = getSymbols();
21
27
  const scriptBuf = toCString(script);
22
- check(lib.quiver_lua_runner_run(this._ptr, scriptBuf.buf));
28
+ const outResult = allocPtrOut();
29
+ check(lib.quiver_lua_runner_run(this._ptr, scriptBuf.buf, outResult.buf));
30
+ const result = decodeStringFromBuf(outResult);
31
+ lib.quiver_lua_runner_free_string(readPtrOut(outResult));
32
+ return result;
23
33
  }
24
34
 
25
35
  close(): void {
@@ -23,3 +23,27 @@ Database.prototype.inTransaction = function (this: Database): boolean {
23
23
  check(lib.quiver_database_in_transaction(this._handle, outBuf));
24
24
  return new DataView(outBuf.buffer).getInt32(0, true) !== 0;
25
25
  };
26
+
27
+ /**
28
+ * Begin a dry run: a transaction that endDryRun always rolls back.
29
+ *
30
+ * While it is active, beginTransaction/commit/rollback are absorbed (no-ops), so code that
31
+ * manages its own transactions composes instead of erroring on a nested BEGIN. A nested rollback
32
+ * is therefore not partial -- everything is undone when the dry run ends.
33
+ */
34
+ Database.prototype.beginDryRun = function (this: Database): void {
35
+ const lib = getSymbols();
36
+ check(lib.quiver_database_begin_dry_run(this._handle));
37
+ };
38
+
39
+ Database.prototype.endDryRun = function (this: Database): void {
40
+ const lib = getSymbols();
41
+ check(lib.quiver_database_end_dry_run(this._handle));
42
+ };
43
+
44
+ Database.prototype.inDryRun = function (this: Database): boolean {
45
+ const lib = getSymbols();
46
+ const outBuf = new Uint8Array(4);
47
+ check(lib.quiver_database_in_dry_run(this._handle, outBuf));
48
+ return new DataView(outBuf.buffer).getInt32(0, true) !== 0;
49
+ };