quiverdb 0.12.8 → 0.13.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
@@ -62,8 +62,9 @@ bun run example.ts
62
62
 
63
63
  ### Lifecycle
64
64
 
65
- - `Database.fromSchema(dbPath, schemaPath)` -- Create database from SQL schema file
66
- - `Database.fromMigrations(dbPath, migrationsPath)` -- Create database from migrations directory
65
+ - `Database.fromSchema(dbPath, schemaPath, options?)` -- Create database from SQL schema file
66
+ - `Database.fromMigrations(dbPath, migrationsPath, options?)` -- Create database from migrations directory
67
+ - `Database.open(dbPath, options?)` -- Open an existing database file; the schema loads on first use
67
68
  - `Database.validateMigrations(migrationsPath)` -- Validate a migrations directory (every `up.sql`, then every `down.sql`, ending with no table left behind) in-memory; throws on failure
68
69
  - `close()` -- Close the database connection
69
70
 
@@ -74,6 +75,16 @@ bun run example.ts
74
75
  - `deleteElement(collection, id)` -- Delete element by ID
75
76
  - `updateElementByLabel(collection, label, data)` -- Update element by label
76
77
  - `deleteElementByLabel(collection, label)` -- Delete element by label
78
+ - `updateRelation(collectionFrom, collectionTo, relationType, id, targetLabel)` -- Point a scalar
79
+ relation at the element labelled `targetLabel` (`null` clears it)
80
+ - `updateRelationByLabel(collectionFrom, collectionTo, relationType, label, targetLabel)` -- Same,
81
+ addressing the element by label
82
+ - `updateVectorGroup(collection, group, id, data)` -- Replace an element's rows in one named vector
83
+ group (`{}` clears it)
84
+ - `updateVectorGroupByLabel(collection, group, label, data)` -- Same, addressing the element by label
85
+ - `updateSetGroup(collection, group, id, data)` -- Replace an element's rows in one named set group
86
+ (`{}` clears it)
87
+ - `updateSetGroupByLabel(collection, group, label, data)` -- Same, addressing the element by label
77
88
 
78
89
  ### Read (bulk)
79
90
 
@@ -127,6 +138,18 @@ bun run example.ts
127
138
 
128
139
  - `readTimeSeriesGroup(collection, group, id)` -- Read time series data for an element
129
140
  - `updateTimeSeriesGroup(collection, group, id, data)` -- Write time series data for an element
141
+ (`{}` clears it)
142
+ - `updateTimeSeriesGroupByLabel(collection, group, label, data)` -- Same, addressing the element by
143
+ label
144
+ - `readTimeSeriesRow(collection, group, attribute, dateTime)` -- One value per element: the last
145
+ non-null value at or before `dateTime`, or `null` if there is none
146
+ - `upsertTimeSeriesRow(collection, group, id, row)` -- Insert or replace one row, keyed by its
147
+ dimension value
148
+ - `upsertTimeSeriesRowByLabel(collection, group, label, row)` -- Same, addressing the element by label
149
+ - `hasTimeSeriesFiles(collection)` -- Check whether the collection has a time series files table
150
+ - `listTimeSeriesFilesColumns(collection)` -- List the time series files columns
151
+ - `readTimeSeriesFiles(collection)` -- Read the file paths (`null` for an unset column)
152
+ - `updateTimeSeriesFiles(collection, data)` -- Write the file paths
130
153
 
131
154
  ### Query
132
155
 
@@ -162,27 +185,31 @@ as an exact INTEGER, a `boolean` as the INTEGER 1 or 0).
162
185
 
163
186
  ### Introspection
164
187
 
165
- - `describe()` -- Print schema info to stdout
188
+ - `describe()` -- Whole-database text report (returns a string)
189
+ - `describeCollection(collection)` -- One collection's structure (returns a string)
190
+ - `summarizeCollection(collection)` -- Per-attribute null/value summary (returns a string)
166
191
  - `isHealthy()` -- Check database health
167
192
  - `path()` -- Get database file path
168
193
  - `currentVersion()` -- Get current schema version
169
194
 
170
195
  ### Lua
171
196
 
172
- - `LuaRunner(db)` -- Create Lua script runner with database access
197
+ - `Sandbox(db)` -- Create Lua script runner with database access
173
198
  - `run(script)` -- Execute a Lua script; returns its return value as a JSON string, or `""` if it
174
199
  returned nothing
175
- - `close()` -- Close the Lua runner
200
+ - `close()` -- Close the Sandbox
176
201
 
177
202
  ## Types
178
203
 
179
204
  Exported types available for TypeScript consumers:
180
205
 
181
- - `ScalarValue` -- `number | bigint | string | null`
182
- - `ArrayValue` -- `number[] | bigint[] | string[]`
206
+ - `ScalarValue` -- `number | bigint | boolean | string | null` (a `boolean` is stored as INTEGER 1/0)
207
+ - `ArrayValue` -- `number[] | bigint[] | boolean[] | string[]`
183
208
  - `Value` -- `ScalarValue | ArrayValue`
184
209
  - `ElementData` -- `Record<string, Value | undefined>`
185
210
  - `QueryParam` -- `number | bigint | boolean | string | null`
211
+ - `GroupColumns` -- `Record<string, (number | bigint | string | boolean | null)[]>`, the column-oriented
212
+ payload of `updateTimeSeriesGroup` / `updateVectorGroup` / `updateSetGroup` (and their `ByLabel` forms)
186
213
  - `QuiverError` -- Error class for all Quiver operations
187
214
  - `ScalarMetadata` -- Scalar attribute metadata
188
215
  - `GroupMetadata` -- Vector/set/time series group metadata
Binary file
Binary file
Binary file
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.12.8",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -12,8 +12,8 @@ export type { CsvOptions } from "./csv.ts";
12
12
  export { Database } from "./database.ts";
13
13
  export { QuiverError } from "./errors.ts";
14
14
  export type { GroupColumns, TimeSeriesData } from "./group-columns.ts";
15
- export { LUA_DB_API_REFERENCE } from "./lua-api.ts";
16
- export { LuaRunner } from "./lua-runner.ts";
15
+ export { SANDBOX_API_REFERENCE } from "./sandbox-api.ts";
16
+ export { Sandbox } from "./sandbox.ts";
17
17
  export type { GroupMetadata, ScalarMetadata } from "./metadata.ts";
18
18
  export type {
19
19
  ArrayValue,
package/src/loader.ts CHANGED
@@ -196,11 +196,11 @@ const freeSymbols = {
196
196
  quiver_database_free_group_metadata_array: { args: [P, USIZE], returns: I32 },
197
197
  } as const;
198
198
 
199
- const luaSymbols = {
200
- quiver_lua_runner_new: { args: [P, P], returns: I32 },
201
- quiver_lua_runner_free: { args: [P], returns: I32 },
202
- quiver_lua_runner_run: { args: [P, BUF, BUF], returns: I32 },
203
- quiver_lua_runner_free_string: { args: [P], returns: I32 },
199
+ const sandboxSymbols = {
200
+ quiver_sandbox_new: { args: [P, P], returns: I32 },
201
+ quiver_sandbox_free: { args: [P], returns: I32 },
202
+ quiver_sandbox_run: { args: [P, BUF, BUF], returns: I32 },
203
+ quiver_sandbox_free_string: { args: [P], returns: I32 },
204
204
  } as const;
205
205
 
206
206
  // Combined symbol map for dlopen.
@@ -216,7 +216,7 @@ const allSymbols = {
216
216
  ...timeSeriesSymbols,
217
217
  ...csvSymbols,
218
218
  ...freeSymbols,
219
- ...luaSymbols,
219
+ ...sandboxSymbols,
220
220
  } as const;
221
221
 
222
222
  // ---------------------------------------------------------------------------
@@ -1,33 +1,33 @@
1
1
  // Agent-facing reference for the Lua `db` API available inside run_lua scripts.
2
2
  //
3
- // Authority: `src/lua_runner.cpp` `bind_database()` (this repo) — extracted by hand, NOT imported.
3
+ // Authority: the binders under `src/sandbox/` (`bind_database` through `bind_expression`,
4
+ // this repo) — extracted by hand, NOT imported.
4
5
  // The shipped quiverdb native binding is the runtime truth; this is docs.
5
6
  //
6
- // SYNC: `test/lua-api-sync.test.ts` derives the bound surface from `src/lua_runner.cpp` and checks
7
+ // SYNC: `test/sandbox-api-sync.test.ts` derives the bound surface from every `.cpp`/`.h` under
8
+ // `src/sandbox/` and checks
7
9
  // it AUTOMATICALLY — every `db:`/`quiver.*` name is documented, no documented name has been
8
10
  // removed, and the stdlib sentence matches `open_libraries` exactly. What it CANNOT check, and you
9
11
  // must still re-diff by hand when the binding changes: arg order, arity, arg types, return shapes,
10
12
  // and whether the prose is semantically true.
11
13
  //
12
14
  // NOTE: the binary/expression subsystems are bound in the native binding and documented below.
13
- // File-touching operations (db:open_file, db:bin_to_csv, db:csv_to_bin, db:validate_migrations,
14
- // expr:save) are sandboxed to the database file's directory; the pure-metadata builders stay under
15
- // the quiver.* global.
15
+ // File-touching operations are sandboxed to the database file's directory (the "Filesystem
16
+ // sandbox" bullet lists all of them); the pure-metadata builders stay under the quiver.* global.
16
17
  //
17
18
  // FORMAT CONVENTION: every db: method appears at least once as the literal token
18
19
  // `db:<snake_case_name>`, and every quiver.* function as `quiver.<name>`, so coverage is greppable
19
20
  // (the sync test relies on this).
20
- export const LUA_DB_API_REFERENCE = `
21
- # Quiver Lua API Reference
21
+ export const SANDBOX_API_REFERENCE = `
22
+ # Quiver Sandbox API Reference
22
23
 
23
- Quiver embeds a Lua scripting layer (via \`LuaRunner\`) that exposes the same database API as the
24
- other language bindings. This document is a complete reference of every method available to a Lua
25
- script.
24
+ Quiver embeds a Lua sandbox scripting layer that exposes the same database API as the other language
25
+ bindings. This document is a complete reference of every method available to a Lua script.
26
26
 
27
27
  ## Running scripts
28
28
 
29
29
  The database is provided to your script as a global userdata named \`db\`. There are **no
30
- open/close lifecycle methods in Lua** — the \`LuaRunner\` opens the database and hands you \`db\`
30
+ open/close lifecycle methods in Lua** — the \`Sandbox\` opens the database and hands you \`db\`
31
31
  already connected. All methods are called with the colon syntax:
32
32
 
33
33
  \`\`\`lua
@@ -100,18 +100,25 @@ midnight.
100
100
  and do not \`pcall\` inside it: a caught error lets the block commit whatever ran.
101
101
  - **Standard library.** Loaded standard libraries: base, string, table, math, coroutine, utf8.
102
102
  That is the pure-computation set — there is no \`os\`, \`io\`, \`debug\`, or \`package\`/\`require\`,
103
- and \`dofile\`/\`loadfile\` are removed (string-form \`load\` stays available). Integer division is
104
- the Lua 5.4 \`//\` operator — a language operator, unrelated to \`math\`. No \`io\` does **not** mean
103
+ and \`dofile\`/\`loadfile\` are removed (string-form \`load\` stays for source text; a precompiled
104
+ binary chunk is refused). Integer division is the Lua 5.4 \`//\` operator — a language operator,
105
+ unrelated to \`math\`. No \`io\` does **not** mean
105
106
  a data file on disk is out of reach: read it with \`db:read_csv\` / \`db:read_csv_stream\` (see
106
107
  the CSV file reading section below). Never copy, paste, or re-type a data file's contents into
107
108
  the script as literals — read the file.
108
109
  - **Filesystem sandbox.** Every file-touching operation (\`db:export_csv\`, \`db:import_csv\`,
109
110
  \`db:open_file\`, \`db:bin_to_csv\`, \`db:csv_to_bin\`, \`db:validate_migrations\`, \`db:read_csv\`,
110
- \`db:read_csv_stream\`, \`db:write_csv\`, \`expr:save\`) resolves
111
+ \`db:read_csv_stream\`, \`db:write_csv\`, \`save\` on a file or an expression) resolves
111
112
  relative paths against the directory containing the database file and rejects anything outside it
112
113
  (subdirectories are fine; \`..\` escapes and outside absolute paths throw \`Cannot <op>: path '...' escapes the
113
114
  database directory ...\`). On an in-memory database these operations throw
114
115
  \`Cannot <op>: database is in-memory, file operations are unavailable\`.
116
+ - **What the sandbox does not limit.** The sandbox controls which files a script can touch and
117
+ which standard libraries exist. It does not bound how much work a script does: there is no
118
+ instruction-count limit, no memory cap and no wall-clock timeout (\`while true do end\` runs
119
+ until the host stops it). Globals persist across \`run()\` calls on the same runner, so a global
120
+ one script sets is visible to the next; use \`local\`. A host that runs untrusted scripts has to
121
+ impose those limits outside the library.
115
122
  - **Output.** A script can \`return\` one value and the host receives it as JSON — prefer this over
116
123
  \`print()\` when you need structured data back (\`print()\` still works and is captured). Only the
117
124
  **first** returned value is encoded. Arrays are 1-indexed. Iterate with \`ipairs\` only where no
@@ -193,7 +200,7 @@ db:begin_transaction() -- start an explicit transaction
193
200
  db:commit() -- commit it
194
201
  db:rollback() -- roll it back
195
202
  db:in_transaction() -- boolean: is a transaction currently open?
196
- db:transaction(fn) -- run fn(db) inside begin/commit; rollback + rethrow if fn errors
203
+ db:transaction(fn) -- run fn(db) inside begin/commit; rollback + rethrow if fn errors or the commit fails
197
204
  \`\`\`
198
205
 
199
206
  To make a group of writes atomic, prefer the \`db:transaction\` wrapper:
@@ -303,8 +310,13 @@ Notes:
303
310
  which only the new label resolves. Because the label form delegates to the id form, failures
304
311
  that validate the *element* (an empty table, a type mismatch) report
305
312
  \`Cannot update_element: ...\`.
306
- - **Empty arrays are skipped.** An attribute whose value is \`{}\` writes no vector/set (the element
307
- type can't be inferred from an empty array), so it is silently dropped.
313
+ - **An empty array clears on update.** On \`update_element\` / \`update_element_by_label\`,
314
+ \`{ col = {} }\` clears the whole group holding \`col\` (all its columns, and every group that
315
+ shares the column name). An empty column beside a non-empty column of the same group throws a
316
+ length error, and a misspelled empty column throws
317
+ \`array '<name>' does not match any vector, set, or time series table ...\`. \`create_element\`
318
+ skips an empty array. To leave a group alone, omit its column: writing back a
319
+ \`read_vectors_by_id\` result clears a group whose read column came back empty or all-NULL.
308
320
  - **Arrays must be dense.** A vector/set read returns a NULL cell as a \`nil\` hole, but an element
309
321
  array cannot carry one: \`create_element\` / \`update_element\` throw \`array '<name>' has a nil
310
322
  hole ...\` rather than cut the array short at the hole. Write NULL cells with
@@ -712,8 +724,8 @@ no header at all, so \`csv.header\` is absent (\`nil\`, not an empty table) and
712
724
  file's first line — useful for a file with a junk title row and/or a units row around the real
713
725
  header (skip them by naming the header row and slicing \`csv.rows\` in the script). A \`header_row\`
714
726
  past the end of the file throws. Passing the separator positionally (\`db:read_csv(path, ";")\`)
715
- throws \`Cannot read_csv: options must be a table\` instead of silently parsing with a comma; an
716
- unknown key, a separator that isn't a single character (or is a quote, CR, LF or NUL — none of
727
+ throws \`Cannot read_csv: options must be a table, got string\` instead of silently parsing with
728
+ a comma; an unknown key, a separator that isn't a single character (or is a quote, CR, LF or NUL — none of
717
729
  those can be a delimiter), a non-string option key, or a \`header_row\` that isn't a
718
730
  non-negative integer also throws.
719
731
 
@@ -870,9 +882,16 @@ File I/O is db-scoped (\`db:open_file\` / \`db:bin_to_csv\` / \`db:csv_to_bin\`)
870
882
  base paths, sandboxed to the database directory (see Critical rules), and \`get_file_path()\` returns
871
883
  the resolved absolute path. The pure-metadata builders and expression constructors live under the
872
884
  global \`quiver\` table. Mirrors the Julia surface; aggregation ops are strings (Lua has no enums);
873
- operators are \`+ - * /\` and unary \`-\`, with scalars allowed on either side. A file handle does not
874
- outlive the \`run()\` that opened it: any handle still open when the script returns is closed (and a
875
- writer flushed), so reopen the file in each script.
885
+ operators are \`+ - * /\` and unary \`-\`, with scalars allowed on either side. A binary file **is** an
886
+ expression: every operator, every \`quiver.*\` expression function and every expression method
887
+ (\`aggregate\`, \`aggregate_agents\`, \`select_agents\`, \`rename_agents\`, \`save\`, \`get_metadata\`) takes
888
+ a file handle directly; \`quiver.expression(f)\` stays as an explicit conversion, and saving from a
889
+ file reads it by path and leaves the handle open. A wrong operand raises \`Cannot <op>: operand must
890
+ be an expression or a binary file, got <type>\`, and extra arguments to a \`quiver.*\` expression
891
+ function (or to an operator metamethod called directly) raise
892
+ \`Cannot <op>: too many arguments (expected N, got M)\`; expression methods ignore extra arguments,
893
+ like every other method. A file handle does not outlive the \`run()\` that opened it: any handle still open when the script returns
894
+ is closed (and a writer flushed), so reopen the file in each script.
876
895
 
877
896
  \`\`\`lua
878
897
  local md = quiver.metadata{
@@ -892,7 +911,9 @@ quiver.metadata_from_toml(text); quiver.metadata_from_element(tbl)
892
911
  db:bin_to_csv(path) -- aggregate=true by default; pass false to keep time dims as columns
893
912
  db:csv_to_bin(path)
894
913
 
895
- local e = (quiver.expression(r) + 10.0) * 2.0 -- files auto-wrap; scalars either side
914
+ local e = (r + 10.0) * 2.0 -- a file is an expression; scalars either side
915
+ local per_stage = r:aggregate("stage", "sum") -- every expression method works on a file too
916
+ r:save(copy_path) -- reads the file by path; r stays open
896
917
  e = quiver.abs(e); e = quiver.sqrt(e) -- also quiver.log / quiver.exp
897
918
  local cond_e = quiver.gt(e, 3.0) -- also quiver.lt/quiver.gte/quiver.lte/quiver.eq/quiver.neq
898
919
  -- all -> 1.0/0.0 per element (NaN operand -> NaN)
@@ -902,7 +923,7 @@ e = e:aggregate("stage", "sum") -- sum/mean/min/max/percent
902
923
  e = e:aggregate("stage", "percentile", 0.9) -- percentile needs the fraction
903
924
  e = e:aggregate_agents("mean") -- collapse the label axis
904
925
  e = e:select_agents({"v2"}); e = e:rename_agents({v1 = "alpha"})
905
- e:save(out_path); e:metadata() -- save path is sandboxed like db:open_file
926
+ e:save(out_path); e:get_metadata() -- save path is sandboxed like db:open_file
906
927
  \`\`\`
907
928
 
908
929
  **\`quiver.metadata{...}\` kwargs and defaults:** \`version\` defaults to \`"1"\`; \`initial_datetime\`
@@ -4,19 +4,19 @@ import { allocPtrOut, decodeStringFromBuf, readPtrOut, toCString } from "./ffi-h
4
4
  import type { NativePointer } from "./loader.ts";
5
5
  import { getSymbols } from "./loader.ts";
6
6
 
7
- export class LuaRunner {
7
+ export class Sandbox {
8
8
  private _ptr: NativePointer;
9
9
  private _closed = false;
10
10
 
11
11
  constructor(db: Database) {
12
12
  const lib = getSymbols();
13
13
  const outRunner = allocPtrOut();
14
- check(lib.quiver_lua_runner_new(db._handle, outRunner.buf));
14
+ check(lib.quiver_sandbox_new(db._handle, outRunner.buf));
15
15
  this._ptr = readPtrOut(outRunner);
16
16
  }
17
17
 
18
18
  /**
19
- * Runs a Lua script and returns its return value encoded as JSON, or "" if it returned nothing.
19
+ * Runs a script and returns its return value encoded as JSON, or "" if it returned nothing.
20
20
  *
21
21
  * To execute a script without keeping its writes, wrap the call in
22
22
  * `db.beginDryRun()` / `db.endDryRun()`.
@@ -26,22 +26,22 @@ export class LuaRunner {
26
26
  const lib = getSymbols();
27
27
  const scriptBuf = toCString(script);
28
28
  const outResult = allocPtrOut();
29
- check(lib.quiver_lua_runner_run(this._ptr, scriptBuf.buf, outResult.buf));
29
+ check(lib.quiver_sandbox_run(this._ptr, scriptBuf.buf, outResult.buf));
30
30
  const result = decodeStringFromBuf(outResult);
31
- lib.quiver_lua_runner_free_string(readPtrOut(outResult));
31
+ lib.quiver_sandbox_free_string(readPtrOut(outResult));
32
32
  return result;
33
33
  }
34
34
 
35
35
  close(): void {
36
36
  if (this._closed) return;
37
37
  const lib = getSymbols();
38
- check(lib.quiver_lua_runner_free(this._ptr));
38
+ check(lib.quiver_sandbox_free(this._ptr));
39
39
  this._closed = true;
40
40
  }
41
41
 
42
42
  private ensureOpen(): void {
43
43
  if (this._closed) {
44
- throw new QuiverError("LuaRunner is closed");
44
+ throw new QuiverError("Sandbox is closed");
45
45
  }
46
46
  }
47
47
  }