quiverdb 0.12.5 → 0.12.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.
- package/README.md +6 -3
- package/libs/linux-x86_64/libquiver.so +0 -0
- package/libs/linux-x86_64/libquiver.so.0 +0 -0
- package/libs/linux-x86_64/libquiver_c.so +0 -0
- package/libs/macos-aarch64/libquiver.0.dylib +0 -0
- package/libs/macos-aarch64/libquiver_c.dylib +0 -0
- package/libs/windows-x86_64/libquiver.dll +0 -0
- package/libs/windows-x86_64/libquiver_c.dll +0 -0
- package/mod.ts +1 -13
- package/package.json +1 -1
- package/src/create.ts +21 -23
- package/src/group-columns.ts +48 -19
- package/src/index.ts +5 -0
- package/src/lua-api.ts +98 -37
- package/src/query.ts +5 -0
- package/src/types.ts +2 -1
package/README.md
CHANGED
|
@@ -135,8 +135,8 @@ bun run example.ts
|
|
|
135
135
|
- `queryBoolean(sql, parameters?)` -- Query returning an INTEGER-backed boolean or null
|
|
136
136
|
- `queryFloat(sql, parameters?)` -- Query returning float or null
|
|
137
137
|
|
|
138
|
-
Parameters are passed as an array of `number | boolean | string | null` (a `
|
|
139
|
-
INTEGER 1 or 0).
|
|
138
|
+
Parameters are passed as an array of `number | bigint | boolean | string | null` (a `bigint` binds
|
|
139
|
+
as an exact INTEGER, a `boolean` as the INTEGER 1 or 0).
|
|
140
140
|
|
|
141
141
|
### Transaction
|
|
142
142
|
|
|
@@ -182,12 +182,15 @@ Exported types available for TypeScript consumers:
|
|
|
182
182
|
- `ArrayValue` -- `number[] | bigint[] | string[]`
|
|
183
183
|
- `Value` -- `ScalarValue | ArrayValue`
|
|
184
184
|
- `ElementData` -- `Record<string, Value | undefined>`
|
|
185
|
-
- `QueryParam` -- `number | string | null`
|
|
185
|
+
- `QueryParam` -- `number | bigint | boolean | string | null`
|
|
186
186
|
- `QuiverError` -- Error class for all Quiver operations
|
|
187
187
|
- `ScalarMetadata` -- Scalar attribute metadata
|
|
188
188
|
- `GroupMetadata` -- Vector/set/time series group metadata
|
|
189
189
|
- `TimeSeriesData` -- Time series column data
|
|
190
190
|
- `CsvOptions` -- CSV import/export options
|
|
191
|
+
- `DatabaseOptions` -- `{ readOnly?: boolean; consoleLevel?: number }` (a `LOG_LEVEL_*` constant)
|
|
192
|
+
- `LOG_LEVEL_DEBUG | INFO | WARN | ERROR | OFF`, `DATA_TYPE_INTEGER | FLOAT | STRING | DATE_TIME | NULL`
|
|
193
|
+
-- constants for `DatabaseOptions.consoleLevel` and metadata `dataType`
|
|
191
194
|
|
|
192
195
|
## Development
|
|
193
196
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/mod.ts
CHANGED
|
@@ -11,16 +11,4 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export
|
|
15
|
-
ArrayValue,
|
|
16
|
-
CsvOptions,
|
|
17
|
-
ElementData,
|
|
18
|
-
GroupColumns,
|
|
19
|
-
GroupMetadata,
|
|
20
|
-
QueryParam,
|
|
21
|
-
ScalarMetadata,
|
|
22
|
-
ScalarValue,
|
|
23
|
-
TimeSeriesData,
|
|
24
|
-
Value,
|
|
25
|
-
} from "./src/index.ts";
|
|
26
|
-
export { Database, LUA_DB_API_REFERENCE, LuaRunner, QuiverError } from "./src/index.ts";
|
|
14
|
+
export * from "./src/index.ts";
|
package/package.json
CHANGED
package/src/create.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readPtrOut,
|
|
9
9
|
toCString,
|
|
10
10
|
} from "./ffi-helpers.ts";
|
|
11
|
-
import { type GroupColumns, updateGroupColumns } from "./group-columns.ts";
|
|
11
|
+
import { type GroupColumns, numericCells, updateGroupColumns } from "./group-columns.ts";
|
|
12
12
|
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
13
13
|
import type { ElementData, Value } from "./types.ts";
|
|
14
14
|
|
|
@@ -17,6 +17,7 @@ type Symbols = ReturnType<typeof getSymbols>;
|
|
|
17
17
|
function setElementArray(
|
|
18
18
|
lib: Symbols,
|
|
19
19
|
elemPtr: NativePointer,
|
|
20
|
+
caller: string,
|
|
20
21
|
name: string,
|
|
21
22
|
values: unknown[],
|
|
22
23
|
): void {
|
|
@@ -39,27 +40,18 @@ function setElementArray(
|
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
if (typeof first === "bigint") {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (typeof first === "boolean") {
|
|
49
|
-
const arr = allocNativeInt64((values as boolean[]).map((v) => (v ? 1 : 0)));
|
|
50
|
-
check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (typeof first === "number") {
|
|
55
|
-
const allIntegers = (values as number[]).every((v) => Number.isInteger(v));
|
|
56
|
-
if (allIntegers) {
|
|
57
|
-
const arr = allocNativeInt64(values as number[]);
|
|
43
|
+
if (typeof first === "number" || typeof first === "boolean" || typeof first === "bigint") {
|
|
44
|
+
// The null check above leaves no null cell, so the cast only narrows.
|
|
45
|
+
const cells = numericCells(caller, name, values) as (number | bigint)[];
|
|
46
|
+
if (cells.every((v) => typeof v === "bigint" || Number.isInteger(v))) {
|
|
47
|
+
const arr = allocNativeInt64(cells);
|
|
58
48
|
check(
|
|
59
49
|
lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null),
|
|
60
50
|
);
|
|
61
51
|
} else {
|
|
62
|
-
|
|
52
|
+
// A fractional cell makes the array FLOAT; a bigint cell then follows the int-for-REAL rule
|
|
53
|
+
// through Number(), exact up to 2^53 like any JS number.
|
|
54
|
+
const arr = allocNativeFloat64(cells.map(Number));
|
|
63
55
|
check(lib.quiver_element_set_array_float(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
|
64
56
|
}
|
|
65
57
|
return;
|
|
@@ -76,7 +68,13 @@ function setElementArray(
|
|
|
76
68
|
throw new QuiverError(`Unsupported array element type for '${name}': ${typeof first}`);
|
|
77
69
|
}
|
|
78
70
|
|
|
79
|
-
function setElementField(
|
|
71
|
+
function setElementField(
|
|
72
|
+
lib: Symbols,
|
|
73
|
+
elemPtr: NativePointer,
|
|
74
|
+
caller: string,
|
|
75
|
+
name: string,
|
|
76
|
+
value: Value,
|
|
77
|
+
): void {
|
|
80
78
|
const nameBuf = toCString(name);
|
|
81
79
|
|
|
82
80
|
if (value === null) {
|
|
@@ -110,7 +108,7 @@ function setElementField(lib: Symbols, elemPtr: NativePointer, name: string, val
|
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
if (Array.isArray(value)) {
|
|
113
|
-
setElementArray(lib, elemPtr, name, value);
|
|
111
|
+
setElementArray(lib, elemPtr, caller, name, value);
|
|
114
112
|
return;
|
|
115
113
|
}
|
|
116
114
|
|
|
@@ -132,7 +130,7 @@ Database.prototype.createElement = function (
|
|
|
132
130
|
try {
|
|
133
131
|
for (const [key, value] of Object.entries(data)) {
|
|
134
132
|
if (value === undefined) continue;
|
|
135
|
-
setElementField(lib, elemPtr, key, value);
|
|
133
|
+
setElementField(lib, elemPtr, "createElement", key, value);
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
const outIdBuf = new Uint8Array(8);
|
|
@@ -160,7 +158,7 @@ Database.prototype.updateElement = function (
|
|
|
160
158
|
try {
|
|
161
159
|
for (const [key, value] of Object.entries(data)) {
|
|
162
160
|
if (value === undefined) continue;
|
|
163
|
-
setElementField(lib, elemPtr, key, value);
|
|
161
|
+
setElementField(lib, elemPtr, "updateElement", key, value);
|
|
164
162
|
}
|
|
165
163
|
const collBuf = toCString(collection);
|
|
166
164
|
check(lib.quiver_database_update_element(handle, collBuf.buf, BigInt(id), elemPtr));
|
|
@@ -186,7 +184,7 @@ Database.prototype.updateElementByLabel = function (
|
|
|
186
184
|
try {
|
|
187
185
|
for (const [key, value] of Object.entries(data)) {
|
|
188
186
|
if (value === undefined) continue;
|
|
189
|
-
setElementField(lib, elemPtr, key, value);
|
|
187
|
+
setElementField(lib, elemPtr, "updateElementByLabel", key, value);
|
|
190
188
|
}
|
|
191
189
|
const collBuf = toCString(collection);
|
|
192
190
|
const labelBuf = toCString(label);
|
package/src/group-columns.ts
CHANGED
|
@@ -24,12 +24,16 @@ import {
|
|
|
24
24
|
DATA_TYPE_STRING,
|
|
25
25
|
} from "./types.ts";
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Column-oriented group payload: one array of cells per column name, `null` for SQL NULL. A
|
|
29
|
+
* `bigint` cell is written as an exact int64 (like createElement); a `boolean` as INTEGER 1/0.
|
|
30
|
+
*/
|
|
31
|
+
export type GroupColumns = Record<string, (number | bigint | string | boolean | null)[]>;
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Column-oriented group read result: one array of cells per column name, `null` for SQL NULL.
|
|
32
|
-
* The read-side twin of GroupColumns; no reader produces a boolean, so it admits
|
|
35
|
+
* The read-side twin of GroupColumns; no reader produces a boolean or a bigint, so it admits
|
|
36
|
+
* neither.
|
|
33
37
|
*/
|
|
34
38
|
export type TimeSeriesData = Record<string, (number | string | null)[]>;
|
|
35
39
|
|
|
@@ -51,6 +55,34 @@ type ColumnUpdateFn = (
|
|
|
51
55
|
rowCount: bigint,
|
|
52
56
|
) => number;
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Check and normalize the cells of a numeric column: one whose first non-null cell is a number, a
|
|
60
|
+
* bigint or a boolean. Shared by updateGroupColumns and setElementArray (create.ts) so the group
|
|
61
|
+
* writers and the element arrays agree on every cell. String columns are never passed here.
|
|
62
|
+
*
|
|
63
|
+
* - A boolean becomes INTEGER 1/0, since SQLite has no boolean type. Per cell, because mapping the
|
|
64
|
+
* whole column from a leading boolean truthiness-mapped the rest and rewrote [true, 5] as
|
|
65
|
+
* [1, 1]. Before the INTEGER/FLOAT choice, because Number.isInteger(true) is false.
|
|
66
|
+
* - A bigint stays a bigint, so an INTEGER column keeps it exact; the caller maps it through
|
|
67
|
+
* Number() only if the column turns out FLOAT.
|
|
68
|
+
* - Any other non-null cell throws, naming the column and the cell. It would fail
|
|
69
|
+
* Number.isInteger, tag the column FLOAT and reach DataView.setFloat64, which converts it with no
|
|
70
|
+
* error: "abc" to NaN, which SQLite stores as NULL, and "2" to 2.
|
|
71
|
+
*/
|
|
72
|
+
export function numericCells(
|
|
73
|
+
caller: string,
|
|
74
|
+
name: string,
|
|
75
|
+
values: readonly unknown[],
|
|
76
|
+
): (number | bigint | null)[] {
|
|
77
|
+
return values.map((v, r) => {
|
|
78
|
+
if (v === null || typeof v === "number" || typeof v === "bigint") return v;
|
|
79
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
80
|
+
throw new QuiverError(
|
|
81
|
+
`Cannot ${caller}: numeric column '${name}' has unsupported value type ${typeof v} in cell ${r}`,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
/**
|
|
55
87
|
* Marshal a column-oriented payload and forward it to one of the columnar group update C
|
|
56
88
|
* functions. Shared by updateTimeSeriesGroup / updateVectorGroup / updateSetGroup and their
|
|
@@ -114,16 +146,7 @@ export function updateGroupColumns(
|
|
|
114
146
|
const maskPtrs: (Pointer | null)[] = [];
|
|
115
147
|
|
|
116
148
|
for (let c = 0; c < columnCount; c++) {
|
|
117
|
-
const [colName,
|
|
118
|
-
// SQLite has no boolean type: a boolean is INTEGER 1/0, as in setElementField and
|
|
119
|
-
// marshalParams. Normalizing per cell before the dispatch (rather than adding a boolean
|
|
120
|
-
// branch after it) is what makes a mixed [true, 5] column write 1 and 5 instead of
|
|
121
|
-
// truthiness-mapping every cell, and matches Python's per-cell `int(v)`. A string column is
|
|
122
|
-
// left alone so normalizing cannot change what a mixed ['a', true] column already wrote.
|
|
123
|
-
const isStringColumn = typeof rawValues.find((v) => v !== null) === "string";
|
|
124
|
-
const values = isStringColumn
|
|
125
|
-
? rawValues
|
|
126
|
-
: rawValues.map((v) => (typeof v === "boolean" ? (v ? 1 : 0) : v));
|
|
149
|
+
const [colName, values] = entries[c];
|
|
127
150
|
const first = values.find((v) => v !== null);
|
|
128
151
|
|
|
129
152
|
// Mask via direct indexing — never a DataView, to avoid the documented
|
|
@@ -141,23 +164,29 @@ export function updateGroupColumns(
|
|
|
141
164
|
keepalive.push(p);
|
|
142
165
|
dataPtrs.push(p.ptr);
|
|
143
166
|
} else if (typeof first === "string") {
|
|
167
|
+
// Not checked by numericCells: a mixed ['a', true] column still writes what it always wrote.
|
|
144
168
|
typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
|
|
145
169
|
const { table, keepalive: strPtrs } = allocNativeStringArray(
|
|
146
170
|
values.map((v) => (v === null ? null : (v as string))),
|
|
147
171
|
);
|
|
148
172
|
keepalive.push(table, ...strPtrs);
|
|
149
173
|
dataPtrs.push(table.ptr);
|
|
150
|
-
} else if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
} else if (
|
|
175
|
+
typeof first === "number" ||
|
|
176
|
+
typeof first === "boolean" ||
|
|
177
|
+
typeof first === "bigint"
|
|
178
|
+
) {
|
|
179
|
+
const cells = numericCells(caller, colName, values);
|
|
180
|
+
if (cells.every((v) => v === null || typeof v === "bigint" || Number.isInteger(v))) {
|
|
154
181
|
typesDv.setInt32(c * 4, DATA_TYPE_INTEGER, true);
|
|
155
|
-
const p = allocNativeInt64(
|
|
182
|
+
const p = allocNativeInt64(cells.map((v) => v ?? 0));
|
|
156
183
|
keepalive.push(p);
|
|
157
184
|
dataPtrs.push(p.ptr);
|
|
158
185
|
} else {
|
|
186
|
+
// A fractional cell makes the column FLOAT; a bigint cell then follows the int-for-REAL
|
|
187
|
+
// rule through Number(), exact up to 2^53 like any JS number.
|
|
159
188
|
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
160
|
-
const p = allocNativeFloat64(
|
|
189
|
+
const p = allocNativeFloat64(cells.map((v) => (v === null ? 0 : Number(v))));
|
|
161
190
|
keepalive.push(p);
|
|
162
191
|
dataPtrs.push(p.ptr);
|
|
163
192
|
}
|
package/src/index.ts
CHANGED
package/src/lua-api.ts
CHANGED
|
@@ -52,7 +52,7 @@ Lua values map to Quiver column values as follows:
|
|
|
52
52
|
| number (float) | REAL | A float is rejected for an INTEGER column. |
|
|
53
53
|
| string | TEXT | Also used for \`date_time\` columns (ISO 8601). |
|
|
54
54
|
| boolean | INTEGER 1/0 | SQLite has no boolean type; \`true\` writes 1, \`false\` 0. |
|
|
55
|
-
| \`nil\` | NULL | In query params
|
|
55
|
+
| \`nil\` | NULL | In query params (not trailing), ts rows, group cells, relations. |
|
|
56
56
|
| table (1-indexed) | array | Used for vectors/sets and column-oriented data.|
|
|
57
57
|
|
|
58
58
|
A boolean is accepted **wherever an integer is** — element scalars and arrays, query parameters,
|
|
@@ -86,10 +86,18 @@ midnight.
|
|
|
86
86
|
- **Type coercion.** An integer is accepted for a REAL column (coerced to real on insert); a float
|
|
87
87
|
is rejected for an INTEGER column. A string bound to a \`date_*\` column must parse as ISO 8601
|
|
88
88
|
(\`YYYY-MM-DD\`, optionally \`THH:MM:SS\` or \` HH:MM:SS\`). Other type mismatches raise a
|
|
89
|
-
validation error
|
|
90
|
-
- **
|
|
91
|
-
\`
|
|
92
|
-
transaction
|
|
89
|
+
validation error.
|
|
90
|
+
- **An uncaught error aborts the script, and the script is not rolled back.** Any error thrown by
|
|
91
|
+
a \`db:\` call that you do not catch with \`pcall\` stops the script and surfaces as
|
|
92
|
+
\`Failed to run Lua script: <message>\`. Outside a transaction each \`db:\` write commits on its
|
|
93
|
+
own, so the failing call changes nothing in the database, but every write that finished before
|
|
94
|
+
it **stays**. Only \`db:transaction(fn)\` and \`db:dry_run(fn)\` undo their block's database
|
|
95
|
+
writes when \`fn\` errors; nothing undoes a file a \`db:\` call wrote. A
|
|
96
|
+
\`db:begin_transaction()\` (or \`db:begin_dry_run()\`) you opened and never closed stays open
|
|
97
|
+
after the error, and closing it is up to the host. A host that runs your script inside its own
|
|
98
|
+
transaction or dry run may undo the whole run; check \`db:in_transaction()\` if it matters. To
|
|
99
|
+
make several writes all-or-nothing, put them in one \`db:transaction(function(db) ... end)\`
|
|
100
|
+
and do not \`pcall\` inside it: a caught error lets the block commit whatever ran.
|
|
93
101
|
- **Standard library.** Loaded standard libraries: base, string, table, math, coroutine, utf8.
|
|
94
102
|
That is the pure-computation set — there is no \`os\`, \`io\`, \`debug\`, or \`package\`/\`require\`,
|
|
95
103
|
and \`dofile\`/\`loadfile\` are removed (string-form \`load\` stays available). Integer division is
|
|
@@ -106,9 +114,12 @@ midnight.
|
|
|
106
114
|
\`Cannot <op>: database is in-memory, file operations are unavailable\`.
|
|
107
115
|
- **Output.** A script can \`return\` one value and the host receives it as JSON — prefer this over
|
|
108
116
|
\`print()\` when you need structured data back (\`print()\` still works and is captured). Only the
|
|
109
|
-
**first** returned value is encoded. Arrays are 1-indexed
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
**first** returned value is encoded. Arrays are 1-indexed. Iterate with \`ipairs\` only where no
|
|
118
|
+
NULL can appear: a read of a nullable column (a bulk scalar read, a vector/set inner list, a
|
|
119
|
+
time-series value column, \`db:read_time_series_row\`) has \`nil\` holes, and \`ipairs\` stops at
|
|
120
|
+
the first one (see Scalar reads). Reading a NULL yields \`nil\`; writing \`nil\` stores NULL where
|
|
121
|
+
NULL is accepted (query params except a trailing one, ts rows, group cells, relation targets —
|
|
122
|
+
but NOT element scalar attributes; see CRUD).
|
|
112
123
|
|
|
113
124
|
\`\`\`lua
|
|
114
125
|
return { ids = db:read_element_ids("Collection"), total = 3 }
|
|
@@ -126,10 +137,10 @@ midnight.
|
|
|
126
137
|
| any other table | object, keys sorted; integer keys stringify |
|
|
127
138
|
|
|
128
139
|
**A table with holes is an object, not an array.** A bulk read of a nullable column returns
|
|
129
|
-
\`nil\` holes (see
|
|
140
|
+
\`nil\` holes (see Scalar reads), so \`return db:read_scalar_integers(c, a)\` encodes as
|
|
130
141
|
\`{"1":10,"3":30}\` — not \`[10,null,30]\` — and the keys sort as text (\`"1","11","2"\`). When the
|
|
131
142
|
host needs positional scalar data, return the ids alongside and fill the holes yourself:
|
|
132
|
-
\`local v = db:read_scalar_integers(c, a); local out = {}; for i
|
|
143
|
+
\`local ids = db:read_element_ids(c); local v = db:read_scalar_integers(c, a); local out = {}; for i = 1, #ids do out[i] = v[i] or false end\`.
|
|
133
144
|
**Inside** a vector/set read an inner list with an interior NULL cell encodes as an object nested
|
|
134
145
|
in the outer array (\`[{"1":10,"3":30},[]]\`), but a trailing NULL cell leaves no trace at all:
|
|
135
146
|
\`[10, NULL]\` encodes as the plain \`[10]\`. The ids count elements, not rows, so they cannot
|
|
@@ -142,8 +153,10 @@ midnight.
|
|
|
142
153
|
self-referencing table); a result over 64 MB raises
|
|
143
154
|
\`Cannot run: script return value exceeds ... bytes of JSON\`; a string holding bytes that are not
|
|
144
155
|
valid UTF-8 raises \`Cannot run: script return value contains a string that is not valid UTF-8\`;
|
|
145
|
-
|
|
146
|
-
raises \`Cannot run: script returned a table with duplicate key '1'
|
|
156
|
+
a table where an integer key and a string key spell the same thing (\`{[1] = 'a', ['1'] = 'b'}\`)
|
|
157
|
+
raises \`Cannot run: script returned a table with duplicate key '1'\`; and a table with a key that
|
|
158
|
+
is neither an integer nor a string (a boolean, a float such as \`1.5\`, a table) raises
|
|
159
|
+
\`Cannot run: script returned a table with an unsupported key type\`.
|
|
147
160
|
- **Embedding harness may restrict further.** The library itself allows transactions and the
|
|
148
161
|
(sandboxed) CSV/file operations below. A host that runs your script (e.g. a hosted \`run_lua\`
|
|
149
162
|
tool) may disable some of them and report \`disabled in the run_lua sandbox\` — that limit comes
|
|
@@ -193,10 +206,12 @@ end) -- both writes commit together; if either throws, both roll back
|
|
|
193
206
|
\`\`\`
|
|
194
207
|
|
|
195
208
|
**Caveat:** if the host already runs your script inside a plain transaction (not a dry run), an
|
|
196
|
-
explicit \`db:begin_transaction()\` will error
|
|
197
|
-
and a mid-script \`db:commit()\` would
|
|
198
|
-
|
|
199
|
-
|
|
209
|
+
explicit \`db:begin_transaction()\` will error
|
|
210
|
+
(\`Cannot begin_transaction: transaction already active\`) and a mid-script \`db:commit()\` would
|
|
211
|
+
prematurely end the host's transaction. \`db:transaction(fn)\` begins one too, so it fails the same
|
|
212
|
+
way. When unsure whether a transaction is already open, check \`db:in_transaction()\` first, or just issue writes directly:
|
|
213
|
+
outside a transaction each \`db:\` write commits on its own, while inside the host's transaction it
|
|
214
|
+
commits or rolls back with the host's.
|
|
200
215
|
|
|
201
216
|
---
|
|
202
217
|
|
|
@@ -235,6 +250,11 @@ Rules worth knowing:
|
|
|
235
250
|
\`Cannot begin_dry_run: dry run already active\` — check \`db:in_dry_run()\` first and skip the
|
|
236
251
|
wrapper when one is already active. Do **not** call \`db:end_dry_run()\` to get around it: that
|
|
237
252
|
ends the host's dry run, and everything you write afterwards is committed for real.
|
|
253
|
+
- **Nor inside a plain transaction.** If the host (or a \`db:transaction\` block) already holds a
|
|
254
|
+
plain transaction, \`db:begin_dry_run()\` and \`db:dry_run(fn)\` throw
|
|
255
|
+
\`Cannot begin_dry_run: transaction already active\`. \`db:in_transaction()\` is true under a host
|
|
256
|
+
dry run *and* under a host transaction, so it is the one check that covers both;
|
|
257
|
+
\`db:in_dry_run()\` covers only the first.
|
|
238
258
|
- For a rough sense of how much a run touched, \`db:query_integer("SELECT total_changes()")\` gives
|
|
239
259
|
the number of rows inserted, updated or deleted on this connection.
|
|
240
260
|
|
|
@@ -293,8 +313,10 @@ Notes:
|
|
|
293
313
|
\`{ x = nil }\` is identical to \`{}\`; an update/create table that ends up with no attributes
|
|
294
314
|
**throws** (\`...must have at least one scalar attribute\` on create, \`...at least one attribute
|
|
295
315
|
to update\` on update). To leave a column unchanged, omit the key — you cannot set a scalar to
|
|
296
|
-
NULL via the element table. (\`nil\` → NULL is
|
|
297
|
-
\`upsert_time_series_row
|
|
316
|
+
NULL via the element table. (\`nil\` → NULL is accepted by \`update_relation\` and in query params
|
|
317
|
+
(not a trailing one), and as a cell in the group writers; \`upsert_time_series_row\` and
|
|
318
|
+
\`update_time_series_files\` replace the whole row, so a column you leave out (or set to \`nil\`,
|
|
319
|
+
the same thing) is cleared — see Time series and Time series files.)
|
|
298
320
|
- **\`update_relation\` points one scalar foreign-key relation at another element**, named by the
|
|
299
321
|
target's label. The column is derived from the naming convention —
|
|
300
322
|
\`lowercase(collection_to) .. "_" .. relation_type\`, so
|
|
@@ -311,7 +333,10 @@ Notes:
|
|
|
311
333
|
|
|
312
334
|
## Scalar reads (bulk, across all elements)
|
|
313
335
|
|
|
314
|
-
Each returns a flat array (1-indexed table), one value per element, in id order.
|
|
336
|
+
Each returns a flat array (1-indexed table), one value per element, in id order. A NULL is a
|
|
337
|
+
\`nil\` hole at that position, so \`ipairs\` stops at the first one and \`#\` is unreliable. Loop
|
|
338
|
+
\`for i = 1, #ids\` over \`local ids = db:read_element_ids(collection)\` and index the result with
|
|
339
|
+
\`i\`.
|
|
315
340
|
|
|
316
341
|
\`\`\`lua
|
|
317
342
|
db:read_scalar_integers(collection, attribute) -- { 42, 37, ... }
|
|
@@ -432,9 +457,10 @@ local values = db:read_time_series_row(collection, group, attribute, date_time)
|
|
|
432
457
|
\`\`\`
|
|
433
458
|
|
|
434
459
|
One value per element using **last non-null value at or before \`date_time\`** semantics. Elements
|
|
435
|
-
with no matching data yield \`nil\` in the array
|
|
436
|
-
|
|
437
|
-
\`
|
|
460
|
+
with no matching data yield \`nil\` in the array, so the result can hold \`nil\` holes: loop over
|
|
461
|
+
\`db:read_element_ids(collection)\` rather than using \`ipairs\`. \`date_time\` is an ISO 8601
|
|
462
|
+
string. A group with more than one dimension column (e.g. \`date_time\` + \`block\`) throws — read
|
|
463
|
+
it with \`read_time_series_group\`.
|
|
438
464
|
|
|
439
465
|
### Replace a whole group (column-oriented — SAME shape as the read)
|
|
440
466
|
|
|
@@ -463,10 +489,10 @@ db:update_time_series_group("Items", "data", id, ts) -- write the whole group
|
|
|
463
489
|
|
|
464
490
|
**DO NOT pass an array of row tables** (\`{ { date_time = ..., value = ... }, ... }\`) — that is the
|
|
465
491
|
\`upsert_time_series_row\` shape, not this one. Doing so raises
|
|
466
|
-
\`
|
|
467
|
-
|
|
492
|
+
\`Cannot update_time_series_group: column names must be strings; pass { column = { values... } }, not an array of row tables\`.
|
|
493
|
+
Each value of the top-level table must be an **array**, not a scalar.
|
|
468
494
|
|
|
469
|
-
**Rules** (
|
|
495
|
+
**Rules** (a violation throws; see "Errors abort the script" for what is kept):
|
|
470
496
|
- Every column value must be an array — a bare scalar throws \`column '...' must be an array of values\`.
|
|
471
497
|
- The **dimension column(s) set the row count** and must be present and fully populated: the
|
|
472
498
|
\`date_*\` ordering column, plus any extra primary-key columns in a multi-dimensional group (e.g.
|
|
@@ -478,8 +504,9 @@ column names). Each value of the top-level table must be an **array**, not a sca
|
|
|
478
504
|
column **longer** than the dimension column throws \`column '...' has length N but expected M\`.
|
|
479
505
|
- Named columns whose dimension transposes to zero rows throw (\`contain no rows; pass an empty
|
|
480
506
|
table {} to clear the group\`) — only a bare \`{}\` clears.
|
|
481
|
-
- Integer values are accepted for REAL columns (converted on insert)
|
|
482
|
-
|
|
507
|
+
- Integer values are accepted for REAL columns (converted on insert), and a boolean is written as
|
|
508
|
+
1/0. A function, a table or another unsupported Lua type throws
|
|
509
|
+
\`column '...' has unsupported Lua type\`.
|
|
483
510
|
- The element id must exist; the \`_by_label\` form takes a label in its place, with
|
|
484
511
|
\`update_element_by_label\`'s resolution and miss semantics. Every rule above applies to both.
|
|
485
512
|
|
|
@@ -503,6 +530,10 @@ db:upsert_time_series_row_by_label("Items", "data", "Item 1", {
|
|
|
503
530
|
The element id must exist; the \`_by_label\` form takes a label in its place, with
|
|
504
531
|
\`update_element_by_label\`'s resolution and miss semantics.
|
|
505
532
|
|
|
533
|
+
An existing row with the same dimension key is **replaced whole**: every value column you leave
|
|
534
|
+
out (or set to \`nil\`) is reset to NULL (or its DEFAULT). To change one value, pass every value
|
|
535
|
+
column of the row.
|
|
536
|
+
|
|
506
537
|
---
|
|
507
538
|
|
|
508
539
|
## Time series files
|
|
@@ -514,10 +545,15 @@ singleton table):
|
|
|
514
545
|
db:has_time_series_files(collection) -- boolean
|
|
515
546
|
db:list_time_series_files_columns(collection) -- { "data_file", "metadata_file", ... }
|
|
516
547
|
db:read_time_series_files(collection) -- { data_file = "path", metadata_file = nil, ... }
|
|
517
|
-
db:update_time_series_files(collection, { data_file = "path/to/data.bin"
|
|
548
|
+
db:update_time_series_files(collection, { data_file = "path/to/data.bin" }) -- metadata_file is cleared
|
|
518
549
|
\`\`\`
|
|
519
550
|
|
|
520
|
-
|
|
551
|
+
\`update_time_series_files\` **replaces the whole row**: every column you do not give a string is
|
|
552
|
+
set to NULL (or to its DEFAULT, if the schema declares one), and in Lua a \`nil\` value and a
|
|
553
|
+
missing key are the same thing. A table with no string values (\`{}\`, or only \`nil\` values)
|
|
554
|
+
changes nothing and clears nothing. A value that is not a string throws. To change one path, read
|
|
555
|
+
the row with \`db:read_time_series_files(collection)\`, change that one entry, and pass the whole
|
|
556
|
+
table back.
|
|
521
557
|
|
|
522
558
|
---
|
|
523
559
|
|
|
@@ -575,9 +611,15 @@ entirely.
|
|
|
575
611
|
## Query (parameterized SQL)
|
|
576
612
|
|
|
577
613
|
Positional \`?\` placeholders; \`params\` is an optional 1-indexed array. Each returns the first
|
|
578
|
-
column of the first row
|
|
614
|
+
column of the first row, or \`nil\` when there is no row, the value is NULL, or it is not already
|
|
615
|
+
the requested type. Only \`query_float\` converts (it widens an INTEGER), so
|
|
616
|
+
\`db:query_string("SELECT COUNT(*) ...")\` and \`db:query_integer("SELECT AVG(x) ...")\` return
|
|
617
|
+
\`nil\`; \`CAST\` in the SQL when unsure (\`SELECT CAST(AVG(x) AS INTEGER)\`). The number of
|
|
579
618
|
\`params\` must match the number of \`?\` placeholders exactly — a mismatch (too few or too many)
|
|
580
|
-
throws rather than binding NULL or ignoring extras.
|
|
619
|
+
throws rather than binding NULL or ignoring extras. A \`nil\` param binds NULL only in a table
|
|
620
|
+
constructor whose last entry is not \`nil\` (\`{ nil, 5 }\`): Lua stores no key for a \`nil\`, so
|
|
621
|
+
\`{ 5, nil }\` or \`{ nil }\` comes up short and throws that mismatch. To test for NULL, write
|
|
622
|
+
\`IS NULL\` in the SQL.
|
|
581
623
|
|
|
582
624
|
\`\`\`lua
|
|
583
625
|
db:query_string(sql, params) -- string or nil
|
|
@@ -599,9 +641,22 @@ local count = db:query_integer("SELECT COUNT(*) FROM Collection")
|
|
|
599
641
|
|
|
600
642
|
## CSV import / export
|
|
601
643
|
|
|
602
|
-
Export
|
|
603
|
-
|
|
604
|
-
|
|
644
|
+
Export one table of \`collection\` to a CSV file, or import one from a CSV file. \`group\` names a
|
|
645
|
+
vector, set or time-series group of \`collection\`; pass \`""\` for the collection's own scalar table
|
|
646
|
+
(the CSV then holds exactly the table's columns except \`id\`, \`label\` included). \`path\` is
|
|
647
|
+
sandboxed: relative paths resolve against the database file's directory and must stay inside it
|
|
648
|
+
(see Critical rules); \`options\` is optional.
|
|
649
|
+
|
|
650
|
+
**\`db:import_csv\` replaces, it does not merge.** A group import deletes every existing row of
|
|
651
|
+
that group, for every element, not only the elements named in the file, and then inserts the
|
|
652
|
+
file's rows. A scalar import (\`group = ""\`) matches elements by exact label. An element whose
|
|
653
|
+
label is in the file keeps its id and has every column overwritten from the file (a blank cell
|
|
654
|
+
writes NULL). **Every element whose label is not in the file is deleted** as
|
|
655
|
+
\`db:delete_element\` would delete it: its group rows go with it, and every relation pointing at
|
|
656
|
+
it from another table follows its \`ON DELETE\` action (\`SET NULL\` clears it, \`CASCADE\`
|
|
657
|
+
deletes the row that holds it, a group row or an element of another collection). So editing a
|
|
658
|
+
label in the file deletes that element and creates a new one. A header-only CSV empties the
|
|
659
|
+
table. To add or change a few elements, use \`db:create_element\` / \`db:update_element\` instead.
|
|
605
660
|
|
|
606
661
|
\`\`\`lua
|
|
607
662
|
db:export_csv(collection, group, path, options)
|
|
@@ -612,7 +667,7 @@ The optional \`options\` table has two keys:
|
|
|
612
667
|
|
|
613
668
|
\`\`\`lua
|
|
614
669
|
{
|
|
615
|
-
date_time_format = "%Y-%m-%d", -- strftime-style format for
|
|
670
|
+
date_time_format = "%Y-%m-%d", -- strftime-style format for every date-time column
|
|
616
671
|
|
|
617
672
|
-- enum_labels: write/read integer codes as human labels. Three nested levels:
|
|
618
673
|
-- attribute name -> locale -> { label = integer_id }
|
|
@@ -625,6 +680,9 @@ The optional \`options\` table has two keys:
|
|
|
625
680
|
}
|
|
626
681
|
\`\`\`
|
|
627
682
|
|
|
683
|
+
An options value that is not a table, an unknown key or a wrong-typed value throws
|
|
684
|
+
(\`Cannot export_csv: unknown option '...'\`).
|
|
685
|
+
|
|
628
686
|
**Precondition:** \`db:import_csv\` cannot run inside an open transaction (it opens and commits its
|
|
629
687
|
own) — it throws \`Cannot import_csv: transaction already active\`. Call it outside any
|
|
630
688
|
\`db:transaction\` / \`db:begin_transaction\` block.
|
|
@@ -812,7 +870,9 @@ File I/O is db-scoped (\`db:open_file\` / \`db:bin_to_csv\` / \`db:csv_to_bin\`)
|
|
|
812
870
|
base paths, sandboxed to the database directory (see Critical rules), and \`get_file_path()\` returns
|
|
813
871
|
the resolved absolute path. The pure-metadata builders and expression constructors live under the
|
|
814
872
|
global \`quiver\` table. Mirrors the Julia surface; aggregation ops are strings (Lua has no enums);
|
|
815
|
-
operators are \`+ - * /\` and unary \`-\`, with scalars allowed on either side.
|
|
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.
|
|
816
876
|
|
|
817
877
|
\`\`\`lua
|
|
818
878
|
local md = quiver.metadata{
|
|
@@ -847,7 +907,8 @@ e:save(out_path); e:metadata() -- save path is sandboxed l
|
|
|
847
907
|
|
|
848
908
|
**\`quiver.metadata{...}\` kwargs and defaults:** \`version\` defaults to \`"1"\`; \`initial_datetime\`
|
|
849
909
|
and \`unit\` default to \`""\`; \`labels\`, \`dimensions\`, \`dimension_sizes\`, \`time_dimensions\`, and
|
|
850
|
-
\`frequencies\` default to empty arrays.
|
|
910
|
+
\`frequencies\` default to empty arrays. Only these eight keys are accepted; an unknown key or a
|
|
911
|
+
value of the wrong type throws.
|
|
851
912
|
|
|
852
913
|
**\`get_dimensions()\` / \`get_metadata()\` dimension shape** — returns an array of dimension tables:
|
|
853
914
|
|
package/src/query.ts
CHANGED
|
@@ -50,6 +50,11 @@ function marshalParams(parameters?: QueryParam[]): {
|
|
|
50
50
|
const native = allocNativeInt64([p ? 1 : 0]);
|
|
51
51
|
keepalive.push(native);
|
|
52
52
|
valuesDv.setBigInt64(i * 8, nativeAddress(native.ptr), true);
|
|
53
|
+
} else if (typeof p === "bigint") {
|
|
54
|
+
typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
|
|
55
|
+
const native = allocNativeInt64([p]);
|
|
56
|
+
keepalive.push(native);
|
|
57
|
+
valuesDv.setBigInt64(i * 8, nativeAddress(native.ptr), true);
|
|
53
58
|
} else if (typeof p === "number") {
|
|
54
59
|
if (Number.isInteger(p)) {
|
|
55
60
|
typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
|
package/src/types.ts
CHANGED
|
@@ -26,7 +26,8 @@ export type ScalarValue = number | bigint | boolean | string | null;
|
|
|
26
26
|
export type ArrayValue = number[] | bigint[] | boolean[] | string[];
|
|
27
27
|
export type Value = ScalarValue | ArrayValue;
|
|
28
28
|
export type ElementData = Record<string, Value | undefined>;
|
|
29
|
-
|
|
29
|
+
/** A `bigint` binds as an exact INTEGER; a `boolean` as INTEGER 1/0. */
|
|
30
|
+
export type QueryParam = number | bigint | boolean | string | null;
|
|
30
31
|
|
|
31
32
|
/** Native memory allocation result. Callers MUST hold `buf` in scope to prevent GC. */
|
|
32
33
|
export type Allocation = {
|