quiverdb 0.9.6 → 0.9.7

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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/mod.ts CHANGED
@@ -22,4 +22,4 @@ export type {
22
22
  TimeSeriesData,
23
23
  Value,
24
24
  } from "./src/index.ts";
25
- export { Database, LuaRunner, QuiverError } from "./src/index.ts";
25
+ export { Database, LUA_DB_API_REFERENCE, LuaRunner, QuiverError } from "./src/index.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/database.ts CHANGED
@@ -180,7 +180,9 @@ export class Database {
180
180
  declare isHealthy: () => boolean;
181
181
  declare currentVersion: () => number;
182
182
  declare path: () => string;
183
- declare describe: () => void;
183
+ declare describe: () => string;
184
+ declare describeCollection: (collection: string) => string;
185
+ declare summarizeCollection: (collection: string) => string;
184
186
 
185
187
  // --- Composite helpers (implemented in composites.ts) ---
186
188
  declare readScalarsById: (
package/src/index.ts CHANGED
@@ -11,16 +11,10 @@ import "./composites.ts";
11
11
  export type { CsvOptions } from "./csv.ts";
12
12
  export { Database } from "./database.ts";
13
13
  export { QuiverError } from "./errors.ts";
14
+ export { LUA_DB_API_REFERENCE } from "./lua-api.ts";
14
15
  export { LuaRunner } from "./lua-runner.ts";
15
16
  export type { GroupMetadata, ScalarMetadata } from "./metadata.ts";
16
17
  export type { TimeSeriesData } from "./time-series.ts";
17
- export {
18
- LOG_LEVEL_DEBUG,
19
- LOG_LEVEL_ERROR,
20
- LOG_LEVEL_INFO,
21
- LOG_LEVEL_OFF,
22
- LOG_LEVEL_WARN,
23
- } from "./types.ts";
24
18
  export type {
25
19
  ArrayValue,
26
20
  DatabaseOptions,
@@ -29,3 +23,10 @@ export type {
29
23
  ScalarValue,
30
24
  Value,
31
25
  } from "./types.ts";
26
+ export {
27
+ LOG_LEVEL_DEBUG,
28
+ LOG_LEVEL_ERROR,
29
+ LOG_LEVEL_INFO,
30
+ LOG_LEVEL_OFF,
31
+ LOG_LEVEL_WARN,
32
+ } from "./types.ts";
@@ -1,6 +1,6 @@
1
1
  import { Database } from "./database.ts";
2
2
  import { check } from "./errors.ts";
3
- import { allocPtrOut, decodeStringFromBuf } from "./ffi-helpers.ts";
3
+ import { allocPtrOut, decodeStringFromBuf, readPtrOut, toCString } from "./ffi-helpers.ts";
4
4
  import { getSymbols } from "./loader.ts";
5
5
 
6
6
  Database.prototype.isHealthy = function (this: Database): boolean {
@@ -24,7 +24,31 @@ Database.prototype.path = function (this: Database): string {
24
24
  return decodeStringFromBuf(outPath);
25
25
  };
26
26
 
27
- Database.prototype.describe = function (this: Database): void {
27
+ Database.prototype.describe = function (this: Database): string {
28
28
  const lib = getSymbols();
29
- check(lib.quiver_database_describe(this._handle));
29
+ const out = allocPtrOut();
30
+ check(lib.quiver_database_describe(this._handle, out.buf));
31
+ const result = decodeStringFromBuf(out);
32
+ lib.quiver_database_free_string(readPtrOut(out));
33
+ return result;
34
+ };
35
+
36
+ Database.prototype.describeCollection = function (this: Database, collection: string): string {
37
+ const lib = getSymbols();
38
+ const collBuf = toCString(collection);
39
+ const out = allocPtrOut();
40
+ check(lib.quiver_database_describe_collection(this._handle, collBuf.buf, out.buf));
41
+ const result = decodeStringFromBuf(out);
42
+ lib.quiver_database_free_string(readPtrOut(out));
43
+ return result;
44
+ };
45
+
46
+ Database.prototype.summarizeCollection = function (this: Database, collection: string): string {
47
+ const lib = getSymbols();
48
+ const collBuf = toCString(collection);
49
+ const out = allocPtrOut();
50
+ check(lib.quiver_database_summarize_collection(this._handle, collBuf.buf, out.buf));
51
+ const result = decodeStringFromBuf(out);
52
+ lib.quiver_database_free_string(readPtrOut(out));
53
+ return result;
30
54
  };
package/src/loader.ts CHANGED
@@ -40,7 +40,7 @@ const lifecycleSymbols = {
40
40
  quiver_database_is_healthy: { args: [P, P], returns: I32 },
41
41
  quiver_database_path: { args: [P, P], returns: I32 },
42
42
  quiver_database_current_version: { args: [P, P], returns: I32 },
43
- quiver_database_describe: { args: [P], returns: I32 },
43
+ quiver_database_describe: { args: [P, P], returns: I32 },
44
44
  } as const;
45
45
 
46
46
  const elementSymbols = {
@@ -112,6 +112,11 @@ const metadataSymbols = {
112
112
  quiver_database_list_time_series_groups: { args: [P, BUF, P, P], returns: I32 },
113
113
  } as const;
114
114
 
115
+ const describeSymbols = {
116
+ quiver_database_describe_collection: { args: [P, BUF, P], returns: I32 },
117
+ quiver_database_summarize_collection: { args: [P, BUF, P], returns: I32 },
118
+ } as const;
119
+
115
120
  const timeSeriesSymbols = {
116
121
  quiver_database_read_time_series_group: { args: [P, BUF, BUF, I64, P, P, P, P, P], returns: I32 },
117
122
  quiver_database_read_time_series_row: { args: [P, BUF, BUF, BUF, BUF, P, P, P], returns: I32 },
@@ -160,6 +165,7 @@ const allSymbols = {
160
165
  ...querySymbols,
161
166
  ...transactionSymbols,
162
167
  ...metadataSymbols,
168
+ ...describeSymbols,
163
169
  ...timeSeriesSymbols,
164
170
  ...csvSymbols,
165
171
  ...freeSymbols,
package/src/lua-api.ts ADDED
@@ -0,0 +1,493 @@
1
+ // Agent-facing reference for the Lua `db` API available inside run_lua scripts.
2
+ //
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).
7
+ //
8
+ // NOTE: the binary/expression subsystems (quiver.* globals + file:/expr: methods) are bound in the
9
+ // native binding and documented below. They are quiver.*/file:/expr: calls, not db:<name> tokens,
10
+ // and they read/write files on the host filesystem.
11
+ //
12
+ // FORMAT CONVENTION: every db: method should appear at least once as the literal token
13
+ // `db:<snake_case_name>` so coverage is greppable.
14
+ export const LUA_DB_API_REFERENCE = `
15
+ # Quiver Lua API Reference
16
+
17
+ Quiver embeds a Lua scripting layer (via \`LuaRunner\`) that exposes the same database API as the
18
+ other language bindings. This document is a complete reference of every method available to a Lua
19
+ script.
20
+
21
+ ## Running scripts
22
+
23
+ The database is provided to your script as a global userdata named \`db\`. There are **no
24
+ open/close lifecycle methods in Lua** — the \`LuaRunner\` opens the database and hands you \`db\`
25
+ already connected. All methods are called with the colon syntax:
26
+
27
+ \`\`\`lua
28
+ db:create_element("Collection", { label = "Item 1", value = 42 })
29
+ local ids = db:read_element_ids("Collection")
30
+ \`\`\`
31
+
32
+ If a script raises an error (including any error thrown by a \`db:\` call), it surfaces to the host
33
+ as:
34
+
35
+ \`\`\`
36
+ Failed to run Lua script: <message>
37
+ \`\`\`
38
+
39
+ ## Value type mapping
40
+
41
+ Lua values map to Quiver column values as follows:
42
+
43
+ | Lua value | Quiver value | Notes |
44
+ | ------------------ | ------------ | ---------------------------------------------- |
45
+ | integer | INTEGER | Lua integers map to int64. |
46
+ | number (float) | REAL | REAL columns reject bare integers: write 75.0, not 75. |
47
+ | string | TEXT | Also used for \`date_time\` columns (ISO 8601). |
48
+ | \`nil\` | NULL | In query params, file paths, and ts rows. |
49
+ | table (1-indexed) | array | Used for vectors/sets and column-oriented data.|
50
+
51
+ **Unsupported types throw.** Passing a boolean, a function, or a nested table where a scalar is
52
+ expected raises an error rather than silently dropping the value. This applies to element
53
+ attributes, time-series rows, and query parameters. A skipped positional query parameter would
54
+ shift every later parameter and bind NULL to the trailing placeholder, so this is rejected loudly.
55
+
56
+ Dates are plain strings in ISO 8601 format: \`YYYY-MM-DDTHH:MM:SS\`. (Lua keeps a string-based
57
+ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Python.)
58
+
59
+ ---
60
+
61
+ ## Critical rules
62
+
63
+ - **STRICT typing.** A REAL column rejects integer literals — write \`75.0\`, not \`75\`; an INTEGER
64
+ column rejects floats. A mismatch raises a validation error and rolls the whole script back.
65
+ - **Errors abort the script.** Any error thrown by a \`db:\` call stops the script and surfaces as
66
+ \`Failed to run Lua script: <message>\`. Validation failures roll back whatever the current
67
+ transaction covered.
68
+ - **Standard library.** Only the \`base\`, \`string\`, and \`table\` libraries are loaded — there is NO
69
+ \`math\` (use \`//\` for integer division), and no \`os\`, \`io\`, \`require\`, \`load\`, or \`dofile\`.
70
+ - **Output.** Scripts cannot return values to the tool — use \`print()\` for anything you need to
71
+ see (it is captured). Arrays are 1-indexed (iterate with \`ipairs\`); reading a NULL yields \`nil\`,
72
+ writing \`nil\` stores NULL where NULL is accepted (query params, ts rows, file columns — but NOT
73
+ element scalar attributes; see CRUD).
74
+
75
+ ---
76
+
77
+ ## Database info
78
+
79
+ \`\`\`lua
80
+ db:is_healthy() -- boolean
81
+ db:current_version() -- integer (current migration version)
82
+ db:path() -- string (database file path)
83
+ db:describe() -- string: whole-DB text report (returns it, does NOT print)
84
+ db:describe_collection(collection) -- string: one collection's structure (text report)
85
+ db:summarize_collection(collection)-- string: per-scalar null/non-null counts, low-cardinality
86
+ -- integer value distributions, per-group sizes
87
+ \`\`\`
88
+
89
+ All three \`describe*\`/\`summarize*\` methods **return** a string — \`print()\` it to see it.
90
+
91
+ ---
92
+
93
+ ## Transactions
94
+
95
+ \`\`\`lua
96
+ db:begin_transaction() -- start an explicit transaction
97
+ db:commit() -- commit it
98
+ db:rollback() -- roll it back
99
+ db:in_transaction() -- boolean: is a transaction currently open?
100
+ db:transaction(fn) -- run fn(db) inside begin/commit; rollback + rethrow if fn errors
101
+ \`\`\`
102
+
103
+ To make a group of writes atomic, prefer the \`db:transaction\` wrapper:
104
+
105
+ \`\`\`lua
106
+ db:transaction(function(db)
107
+ local id = db:create_element("Collection", { label = "Item 1", some_integer = 42 })
108
+ db:update_element("Collection", id, { some_integer = 99 })
109
+ end) -- both writes commit together; if either throws, both roll back
110
+ \`\`\`
111
+
112
+ **Caveat:** if the host already runs your script inside a transaction, an explicit
113
+ \`db:begin_transaction()\` will error (\`cannot start a transaction within a transaction\`) and a
114
+ mid-script \`db:commit()\` would prematurely end the host's transaction. When unsure whether a
115
+ transaction is already open, check \`db:in_transaction()\` first, or just issue writes directly —
116
+ each \`db:\` write is durable on its own.
117
+
118
+ ---
119
+
120
+ ## CRUD
121
+
122
+ \`\`\`lua
123
+ local id = db:create_element(collection, element_table) -- returns new integer id
124
+ db:update_element(collection, id, element_table)
125
+ db:delete_element(collection, id)
126
+ \`\`\`
127
+
128
+ The element table holds scalar attributes as \`key = value\`, and vector/set attributes as
129
+ 1-indexed arrays. The element type of an array is inferred from its **first** entry:
130
+
131
+ \`\`\`lua
132
+ local id = db:create_element("Collection", {
133
+ label = "Item 1", -- scalar string
134
+ some_integer = 42, -- scalar integer
135
+ some_float = 3.14, -- scalar real
136
+ value_int = { 1, 2, 3 }, -- vector/set of integers
137
+ tags = { "a", "b", "c" }, -- vector/set of strings
138
+ })
139
+ \`\`\`
140
+
141
+ \`update_element\` only touches the attributes you pass:
142
+
143
+ \`\`\`lua
144
+ db:update_element("Collection", id, { some_integer = 999 })
145
+ \`\`\`
146
+
147
+ Notes:
148
+ - **Empty arrays are skipped.** An attribute whose value is \`{}\` writes no vector/set (the element
149
+ type can't be inferred from an empty array), so it is silently dropped.
150
+ - **No \`nil\` scalar attributes.** Passing \`nil\` for a scalar attribute **throws** (unsupported
151
+ type). To leave a column unset, omit the key — you cannot set a scalar to NULL via the element
152
+ table. (\`nil\` → NULL is only accepted by \`add_time_series_row\` and \`update_time_series_files\`.)
153
+
154
+ ---
155
+
156
+ ## Scalar reads (bulk, across all elements)
157
+
158
+ Each returns a flat array (1-indexed table), one value per element, in id order.
159
+
160
+ \`\`\`lua
161
+ db:read_scalar_integers(collection, attribute) -- { 42, 37, ... }
162
+ db:read_scalar_floats(collection, attribute) -- { 3.14, 2.71, ... }
163
+ db:read_scalar_strings(collection, attribute) -- { "Item 1", "Item 2", ... }
164
+ \`\`\`
165
+
166
+ ---
167
+
168
+ ## Vector reads (bulk)
169
+
170
+ Each returns an array of arrays — one inner array per element.
171
+
172
+ \`\`\`lua
173
+ db:read_vector_integers(collection, attribute) -- { {1,2,3}, {2,3,4}, ... }
174
+ db:read_vector_floats(collection, attribute)
175
+ db:read_vector_strings(collection, attribute)
176
+ \`\`\`
177
+
178
+ ---
179
+
180
+ ## Set reads (bulk)
181
+
182
+ Same shape as vector reads — an array of arrays.
183
+
184
+ \`\`\`lua
185
+ db:read_set_integers(collection, attribute)
186
+ db:read_set_floats(collection, attribute)
187
+ db:read_set_strings(collection, attribute)
188
+ \`\`\`
189
+
190
+ ---
191
+
192
+ ## Composite by-id reads (Lua convenience helpers)
193
+
194
+ \`\`\`lua
195
+ db:read_element_ids(collection) -- { 1, 2, 3, ... }
196
+
197
+ db:read_scalars_by_id(collection, id) -- { attr = value, ... } (missing -> nil)
198
+ db:read_vectors_by_id(collection, id) -- { column = { v1, v2, ... }, ... }
199
+ db:read_sets_by_id(collection, id) -- { column = { v1, v2, ... }, ... }
200
+ db:read_element_by_id(collection, id) -- scalars + vectors + sets merged into one table
201
+ \`\`\`
202
+
203
+ \`read_element_by_id\` merges every scalar, vector, and set for the element into a single table.
204
+ Scalar attributes with no value come back as \`nil\`.
205
+
206
+ ---
207
+
208
+ ## Time series
209
+
210
+ Time-series group data is **column-oriented** in Lua: \`{ column = { v1, v2, ... }, ... }\`. The
211
+ dimension (ordering) column is a \`date_*\` text column holding ISO 8601 timestamps.
212
+
213
+ ### Read a whole group (column-oriented)
214
+
215
+ \`\`\`lua
216
+ local ts = db:read_time_series_group(collection, group, id)
217
+ -- ts = { date_time = { "2024-01-01T00:00:00", ... }, value = { 10.5, 20.0, ... } }
218
+ -- returns an empty table {} if the element has no rows
219
+ \`\`\`
220
+
221
+ ### Read one value per element at a date (\`read_time_series_row\`)
222
+
223
+ \`\`\`lua
224
+ local values = db:read_time_series_row(collection, group, attribute, date_time)
225
+ -- { v_elem1, v_elem2, ... } in element-id order
226
+ \`\`\`
227
+
228
+ One value per element using **last non-null value at or before \`date_time\`** semantics. Elements
229
+ with no matching data yield \`nil\` in the array. \`date_time\` is an ISO 8601 string.
230
+
231
+ ### Replace a whole group (column-oriented — SAME shape as the read)
232
+
233
+ \`update_time_series_group\` takes the **exact column-oriented shape \`read_time_series_group\`
234
+ returns**: a table mapping each column name to a 1-indexed array of its values. Read → modify →
235
+ write round-trips. Passing an empty table \`{}\` clears the group.
236
+
237
+ \`\`\`lua
238
+ db:update_time_series_group("Items", "data", id, {
239
+ date_time = { "2024-01-01T00:00:00", "2024-01-02T00:00:00", "2024-01-03T00:00:00" },
240
+ value = { 10.5, 20.0, 30.0 },
241
+ })
242
+
243
+ db:update_time_series_group("Items", "data", id, {}) -- clears the group
244
+ \`\`\`
245
+
246
+ A read-modify-write looks like this:
247
+
248
+ \`\`\`lua
249
+ local ts = db:read_time_series_group("Items", "data", id)
250
+ ts.value[2] = 125.0 -- edit the 2nd row's value
251
+ db:update_time_series_group("Items", "data", id, ts) -- write the whole group back
252
+ \`\`\`
253
+
254
+ **DO NOT pass an array of row tables** (\`{ { date_time = ..., value = ... }, ... }\`) — that is the
255
+ \`add_time_series_row\` shape, not this one. Doing so raises
256
+ \`stack index -1, expected string, received number\` (the integer array indices 1, 2, 3 are not
257
+ column names). Each value of the top-level table must be an **array**, not a scalar.
258
+
259
+ **Rules** (validation throws, rolling the script back):
260
+ - Every column value must be an array — a bare scalar throws \`column '...' must be an array of values\`.
261
+ - All columns must have the **same length** — a mismatch throws \`column '...' has length N but expected M\`.
262
+ - Named-but-empty columns throw (\`contain no rows; pass an empty table {} to clear\`) rather than
263
+ silently clearing — only a bare \`{}\` clears.
264
+ - Write REAL-column values as float literals (\`30.0\`, not \`30\`); STRICT validation rejects an
265
+ integer for a REAL column. Other Lua types throw \`column '...' has unsupported Lua type\`.
266
+ - \`nil\` is **not** accepted inside a column here (it throws \`unsupported Lua type\`); columns must
267
+ be fully populated. To write a NULL into a single row, use \`add_time_series_row\` instead, which
268
+ does accept \`nil\`.
269
+
270
+ ### Append/upsert a single row (\`add_time_series_row\` — ROW-oriented, the one exception)
271
+
272
+ Unlike the column-oriented group update above, this takes **one row table of scalars** (dimension
273
+ column + value column(s)), and upserts that single row:
274
+
275
+ \`\`\`lua
276
+ db:add_time_series_row("Items", "data", id, {
277
+ date_time = "2024-01-04T00:00:00",
278
+ value = 40.0,
279
+ })
280
+ \`\`\`
281
+
282
+ ---
283
+
284
+ ## Time series files
285
+
286
+ For schemas that reference external time-series files (the \`{Collection}_time_series_files\`
287
+ singleton table):
288
+
289
+ \`\`\`lua
290
+ db:has_time_series_files(collection) -- boolean
291
+ db:list_time_series_files_columns(collection) -- { "data_file", "metadata_file", ... }
292
+ db:read_time_series_files(collection) -- { data_file = "path", metadata_file = nil, ... }
293
+ db:update_time_series_files(collection, { data_file = "path/to/data.bin", metadata_file = nil })
294
+ \`\`\`
295
+
296
+ In \`update_time_series_files\`, a \`nil\` value clears that column.
297
+
298
+ ---
299
+
300
+ ## Metadata
301
+
302
+ ### Single attribute / group
303
+
304
+ \`\`\`lua
305
+ db:get_scalar_metadata(collection, attribute) -- scalar metadata table (below)
306
+ db:get_vector_metadata(collection, group_name) -- group metadata table (below)
307
+ db:get_set_metadata(collection, group_name) -- group metadata table
308
+ db:get_time_series_metadata(collection, group_name) -- group metadata table (+ dimension_column)
309
+ \`\`\`
310
+
311
+ ### Lists (one entry per attribute/group)
312
+
313
+ \`\`\`lua
314
+ db:list_scalar_attributes(collection) -- array of scalar metadata tables
315
+ db:list_vector_groups(collection) -- array of group metadata tables
316
+ db:list_set_groups(collection) -- array of group metadata tables
317
+ db:list_time_series_groups(collection) -- array of group metadata tables (+ dimension_column)
318
+ \`\`\`
319
+
320
+ ### Scalar metadata table shape
321
+
322
+ \`\`\`lua
323
+ {
324
+ name = "value",
325
+ data_type = "integer", -- "integer" | "real" | "text" | "date_time"
326
+ not_null = true,
327
+ primary_key = false,
328
+ default_value = nil, -- string, or nil
329
+ is_foreign_key = false,
330
+ references_collection = nil, -- string, or nil
331
+ references_column = nil, -- string, or nil
332
+ }
333
+ \`\`\`
334
+
335
+ ### Group metadata table shape
336
+
337
+ \`\`\`lua
338
+ {
339
+ group_name = "data",
340
+ value_columns = { <scalar metadata table>, ... },
341
+ dimension_column = "date_time", -- present ONLY for time-series groups
342
+ }
343
+ \`\`\`
344
+
345
+ The \`dimension_column\` key is present only in **time-series** group metadata
346
+ (\`get_time_series_metadata\` / \`list_time_series_groups\`). Vector and set group metadata omit it
347
+ entirely.
348
+
349
+ ---
350
+
351
+ ## Query (parameterized SQL)
352
+
353
+ Positional \`?\` placeholders; \`params\` is an optional 1-indexed array. Each returns the first
354
+ column of the first row as the requested type, or \`nil\` if there is no result.
355
+
356
+ \`\`\`lua
357
+ db:query_string(sql, params) -- string or nil
358
+ db:query_integer(sql, params) -- integer or nil
359
+ db:query_float(sql, params) -- number or nil
360
+ \`\`\`
361
+
362
+ Example:
363
+
364
+ \`\`\`lua
365
+ local label = db:query_string(
366
+ "SELECT label FROM Collection WHERE some_integer = ?",
367
+ { 42 }
368
+ )
369
+ local count = db:query_integer("SELECT COUNT(*) FROM Collection")
370
+ \`\`\`
371
+
372
+ ---
373
+
374
+ ## CSV import / export
375
+
376
+ Export a time-series group to a CSV file, or import one from a CSV file. \`path\` is resolved by the
377
+ host filesystem; \`options\` is optional.
378
+
379
+ \`\`\`lua
380
+ db:export_csv(collection, group, path, options)
381
+ db:import_csv(collection, group, path, options)
382
+ \`\`\`
383
+
384
+ The optional \`options\` table has two keys:
385
+
386
+ \`\`\`lua
387
+ {
388
+ date_time_format = "%Y-%m-%d", -- strftime-style format for the dimension column
389
+
390
+ -- enum_labels: write/read integer codes as human labels. Three nested levels:
391
+ -- attribute name -> locale -> { label = integer_id }
392
+ enum_labels = {
393
+ status = {
394
+ en = { active = 1, inactive = 0 },
395
+ pt = { ativo = 1, inativo = 0 },
396
+ },
397
+ },
398
+ }
399
+ \`\`\`
400
+
401
+ **Precondition:** \`db:import_csv\` cannot run inside an open transaction (it toggles
402
+ \`PRAGMA foreign_keys\`, a no-op mid-transaction) — it throws \`Cannot import_csv: transaction already
403
+ active\`. Call it outside any \`db:transaction\` / \`db:begin_transaction\` block.
404
+
405
+ ---
406
+
407
+ ## Complete example
408
+
409
+ \`\`\`lua
410
+ -- create
411
+ db:create_element("Configuration", { label = "Configuration" })
412
+
413
+ local item1 = db:create_element("Collection",
414
+ { label = "Item 1", some_integer = 42, some_float = 3.14, value_int = { 1, 2, 3 } })
415
+ local item2 = db:create_element("Collection",
416
+ { label = "Item 2", some_integer = 37, some_float = 2.71, value_int = { 2, 3, 4 } })
417
+
418
+ -- read every element
419
+ local ids = db:read_element_ids("Collection")
420
+ for _, id in ipairs(ids) do
421
+ local element = db:read_element_by_id("Collection", id)
422
+ print("Element " .. id .. ", label = " .. tostring(element.label))
423
+ end
424
+
425
+ -- update
426
+ db:update_element("Collection", item1, { some_integer = 999 })
427
+
428
+ -- delete
429
+ db:delete_element("Collection", item2)
430
+ \`\`\`
431
+
432
+ ---
433
+
434
+ ## Binary & expression subsystems
435
+
436
+ Dense N-dimensional \`float64\` arrays (\`.qvr\` + \`.toml\` sidecar) plus lazy arithmetic over them,
437
+ under a global \`quiver\` table (not \`db\`). Mirrors the Julia surface; aggregation ops are strings
438
+ (Lua has no enums); operators are \`+ - * /\` and unary \`-\`, with scalars allowed on either side.
439
+ These read/write files on the host filesystem (\`path\` is host-resolved).
440
+
441
+ \`\`\`lua
442
+ local md = quiver.metadata{
443
+ initial_datetime = "2025-01-01T00:00:00", unit = "MW",
444
+ labels = {"v1", "v2"}, dimensions = {"stage", "block"}, dimension_sizes = {4, 31},
445
+ time_dimensions = {"stage", "block"}, frequencies = {"monthly", "daily"},
446
+ }
447
+ local f = quiver.open_file(path, "w", md) -- mode "r"/"w"; md required for "w"
448
+ f:write({1.0, 2.0}, {stage = 1, block = 1}) -- data table, dims table
449
+ f:close()
450
+ local r = quiver.open_file(path, "r")
451
+ local cell = r:read({stage = 1, block = 1}) -- { v1, v2 }; pass true as 2nd arg to allow NaN
452
+ r:get_metadata(); r:get_file_path(); r:is_open()
453
+ md:get_unit(); md:get_version(); md:get_initial_datetime()
454
+ md:get_labels(); md:get_dimensions(); md:get_number_of_time_dimensions(); md:to_toml()
455
+ quiver.metadata_from_toml(text); quiver.metadata_from_element(tbl)
456
+ quiver.bin_to_csv(path) -- aggregate=true by default; pass false to keep time dims as columns
457
+ quiver.csv_to_bin(path)
458
+
459
+ local e = (quiver.expression(r) + 10.0) * 2.0 -- files auto-wrap; scalars either side
460
+ e = quiver.abs(e); e = quiver.sqrt(e) -- also quiver.log / quiver.exp
461
+ e = quiver.ifelse(cond_e, then_e, else_e)
462
+ e = e:aggregate("stage", "sum") -- sum/mean/min/max/percentile
463
+ e = e:aggregate("stage", "percentile", 0.9) -- percentile needs the fraction
464
+ e = e:aggregate_agents("mean") -- collapse the label axis
465
+ e = e:select_agents({"v2"}); e = e:rename_agents({v1 = "alpha"})
466
+ e:save(out_path); e:metadata()
467
+ \`\`\`
468
+
469
+ **\`quiver.metadata{...}\` kwargs and defaults:** \`version\` defaults to \`"1"\`; \`initial_datetime\`
470
+ and \`unit\` default to \`""\`; \`labels\`, \`dimensions\`, \`dimension_sizes\`, \`time_dimensions\`, and
471
+ \`frequencies\` default to empty arrays.
472
+
473
+ **\`get_dimensions()\` / \`get_metadata()\` dimension shape** — returns an array of dimension tables:
474
+
475
+ \`\`\`lua
476
+ {
477
+ name = "stage",
478
+ size = 4,
479
+ is_time_dimension = true,
480
+ frequency = "monthly", -- nil for non-time dimensions
481
+ initial_value = 1, -- nil for non-time dimensions
482
+ parent_dimension_index = -1, -- nil for non-time dimensions
483
+ }
484
+ \`\`\`
485
+
486
+ ---
487
+
488
+ ## What Lua does *not* expose
489
+
490
+ DateTime wrapper helpers (Lua uses ISO 8601 strings) and \`_by_id\` single-scalar variants (use the
491
+ composite by-id readers or the bulk readers instead). Everything else the native binding exposes —
492
+ CRUD, reads, time series, metadata, query, CSV, and the binary/expression subsystems — is
493
+ documented above and callable.`;