quiverdb 0.9.15 → 0.10.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 +7 -1
- 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/package.json +2 -2
- package/src/create.ts +52 -2
- package/src/database.ts +17 -0
- package/src/group-columns.ts +161 -0
- package/src/index.ts +1 -0
- package/src/loader.ts +14 -1
- package/src/lua-api.ts +129 -19
- package/src/lua-runner.ts +13 -3
- package/src/read.ts +8 -0
- package/src/time-series.ts +9 -127
- package/src/transaction.ts +24 -0
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ bun run example.ts
|
|
|
99
99
|
### Read (IDs)
|
|
100
100
|
|
|
101
101
|
- `readElementIds(collection)` -- Read all element IDs in a collection
|
|
102
|
+
- `numberOfElements(collection)` -- Current number of elements in a collection
|
|
102
103
|
|
|
103
104
|
### Metadata
|
|
104
105
|
|
|
@@ -130,6 +131,10 @@ Parameters are passed as an array of `number | string | null`.
|
|
|
130
131
|
- `commit()` -- Commit current transaction
|
|
131
132
|
- `rollback()` -- Rollback current transaction
|
|
132
133
|
- `inTransaction()` -- Check if transaction is active
|
|
134
|
+
- `beginDryRun()` -- Begin a dry run: a transaction `endDryRun()` always rolls back. While it is
|
|
135
|
+
active, `beginTransaction`/`commit`/`rollback` are absorbed (no-ops)
|
|
136
|
+
- `endDryRun()` -- End the active dry run, rolling back everything it covered
|
|
137
|
+
- `inDryRun()` -- Check if a dry run is active
|
|
133
138
|
|
|
134
139
|
### CSV
|
|
135
140
|
|
|
@@ -152,7 +157,8 @@ Parameters are passed as an array of `number | string | null`.
|
|
|
152
157
|
### Lua
|
|
153
158
|
|
|
154
159
|
- `LuaRunner(db)` -- Create Lua script runner with database access
|
|
155
|
-
- `run(script)` -- Execute a Lua script
|
|
160
|
+
- `run(script)` -- Execute a Lua script; returns its return value as a JSON string, or `""` if it
|
|
161
|
+
returned nothing
|
|
156
162
|
- `close()` -- Close the Lua runner
|
|
157
163
|
|
|
158
164
|
## Types
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/create.ts
CHANGED
|
@@ -8,6 +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
12
|
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
12
13
|
import type { ElementData, Value } from "./types.ts";
|
|
13
14
|
|
|
@@ -38,7 +39,9 @@ function setElementArray(
|
|
|
38
39
|
const allIntegers = (values as number[]).every((v) => Number.isInteger(v));
|
|
39
40
|
if (allIntegers) {
|
|
40
41
|
const arr = allocNativeInt64(values as number[]);
|
|
41
|
-
check(
|
|
42
|
+
check(
|
|
43
|
+
lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null),
|
|
44
|
+
);
|
|
42
45
|
} else {
|
|
43
46
|
const arr = allocNativeFloat64(values as number[]);
|
|
44
47
|
check(lib.quiver_element_set_array_float(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
|
@@ -48,7 +51,9 @@ function setElementArray(
|
|
|
48
51
|
|
|
49
52
|
if (typeof first === "string") {
|
|
50
53
|
const { table, keepalive: _keepalive } = allocNativeStringArray(values as string[]);
|
|
51
|
-
check(
|
|
54
|
+
check(
|
|
55
|
+
lib.quiver_element_set_array_string(elemPtr, nameBuf.buf, table.buf, values.length, null),
|
|
56
|
+
);
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -148,3 +153,48 @@ Database.prototype.deleteElement = function (this: Database, collection: string,
|
|
|
148
153
|
const collBuf = toCString(collection);
|
|
149
154
|
check(lib.quiver_database_delete_element(this._handle, collBuf.buf, BigInt(id)));
|
|
150
155
|
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Replace all of an element's rows in one *named* vector group, from column arrays keyed by name.
|
|
159
|
+
*
|
|
160
|
+
* Pass `{}` to clear the group. Prefer this over routing the group's columns through
|
|
161
|
+
* updateElement when a column name is shared by two groups of the collection (legal for foreign
|
|
162
|
+
* keys): (collection, group) names exactly one table, a column name alone does not, and
|
|
163
|
+
* updateElement writes an ambiguous column to every match.
|
|
164
|
+
*/
|
|
165
|
+
Database.prototype.updateVectorGroup = function (
|
|
166
|
+
this: Database,
|
|
167
|
+
collection: string,
|
|
168
|
+
group: string,
|
|
169
|
+
id: number,
|
|
170
|
+
data: GroupColumns,
|
|
171
|
+
): void {
|
|
172
|
+
updateGroupColumns(
|
|
173
|
+
this._handle,
|
|
174
|
+
"updateVectorGroup",
|
|
175
|
+
getSymbols().quiver_database_update_vector_group,
|
|
176
|
+
collection,
|
|
177
|
+
group,
|
|
178
|
+
id,
|
|
179
|
+
data,
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Set-group counterpart of updateVectorGroup. */
|
|
184
|
+
Database.prototype.updateSetGroup = function (
|
|
185
|
+
this: Database,
|
|
186
|
+
collection: string,
|
|
187
|
+
group: string,
|
|
188
|
+
id: number,
|
|
189
|
+
data: GroupColumns,
|
|
190
|
+
): void {
|
|
191
|
+
updateGroupColumns(
|
|
192
|
+
this._handle,
|
|
193
|
+
"updateSetGroup",
|
|
194
|
+
getSymbols().quiver_database_update_set_group,
|
|
195
|
+
collection,
|
|
196
|
+
group,
|
|
197
|
+
id,
|
|
198
|
+
data,
|
|
199
|
+
);
|
|
200
|
+
};
|
package/src/database.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CsvOptions } from "./csv.ts";
|
|
2
2
|
import { check, QuiverError } from "./errors.ts";
|
|
3
3
|
import { allocPtrOut, makeDefaultOptions, readPtrOut, toCString } from "./ffi-helpers.ts";
|
|
4
|
+
import type { GroupColumns } from "./group-columns.ts";
|
|
4
5
|
import type { NativePointer } from "./loader.ts";
|
|
5
6
|
import { getSymbols } from "./loader.ts";
|
|
6
7
|
import type { GroupMetadata, ScalarMetadata } from "./metadata.ts";
|
|
@@ -103,6 +104,7 @@ export class Database {
|
|
|
103
104
|
id: number,
|
|
104
105
|
) => string | null;
|
|
105
106
|
declare readElementIds: (collection: string) => number[];
|
|
107
|
+
declare numberOfElements: (collection: string) => number;
|
|
106
108
|
declare readVectorIntegers: (collection: string, attribute: string) => number[][];
|
|
107
109
|
declare readVectorFloats: (collection: string, attribute: string) => number[][];
|
|
108
110
|
declare readVectorStrings: (collection: string, attribute: string) => string[][];
|
|
@@ -126,6 +128,9 @@ export class Database {
|
|
|
126
128
|
declare commit: () => void;
|
|
127
129
|
declare rollback: () => void;
|
|
128
130
|
declare inTransaction: () => boolean;
|
|
131
|
+
declare beginDryRun: () => void;
|
|
132
|
+
declare endDryRun: () => void;
|
|
133
|
+
declare inDryRun: () => boolean;
|
|
129
134
|
|
|
130
135
|
// --- Metadata (implemented in metadata.ts) ---
|
|
131
136
|
declare getScalarMetadata: (collection: string, attribute: string) => ScalarMetadata;
|
|
@@ -151,6 +156,18 @@ export class Database {
|
|
|
151
156
|
id: number,
|
|
152
157
|
data: TimeSeriesData,
|
|
153
158
|
) => void;
|
|
159
|
+
declare updateVectorGroup: (
|
|
160
|
+
collection: string,
|
|
161
|
+
group: string,
|
|
162
|
+
id: number,
|
|
163
|
+
data: GroupColumns,
|
|
164
|
+
) => void;
|
|
165
|
+
declare updateSetGroup: (
|
|
166
|
+
collection: string,
|
|
167
|
+
group: string,
|
|
168
|
+
id: number,
|
|
169
|
+
data: GroupColumns,
|
|
170
|
+
) => void;
|
|
154
171
|
declare upsertTimeSeriesRow: (
|
|
155
172
|
collection: string,
|
|
156
173
|
group: string,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type Pointer, ptr } from "bun:ffi";
|
|
2
|
+
import { check, QuiverError } from "./errors.ts";
|
|
3
|
+
import {
|
|
4
|
+
allocNativeFloat64,
|
|
5
|
+
allocNativeInt64,
|
|
6
|
+
allocNativePtrTable,
|
|
7
|
+
allocNativeStringArray,
|
|
8
|
+
toCString,
|
|
9
|
+
} from "./ffi-helpers.ts";
|
|
10
|
+
import type { NativePointer } from "./loader.ts";
|
|
11
|
+
import { DATA_TYPE_FLOAT, DATA_TYPE_INTEGER, DATA_TYPE_STRING, type Allocation } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/** Column-oriented group payload: one array of cells per column name, `null` for SQL NULL. */
|
|
14
|
+
export type GroupColumns = Record<string, (number | string | null)[]>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The parallel-array signature every columnar group update C function shares
|
|
18
|
+
* (quiver_database_update_{time_series,vector,set}_group).
|
|
19
|
+
*/
|
|
20
|
+
type ColumnUpdateFn = (
|
|
21
|
+
db: NativePointer,
|
|
22
|
+
collection: Uint8Array,
|
|
23
|
+
group: Uint8Array,
|
|
24
|
+
id: bigint,
|
|
25
|
+
names: Uint8Array | null,
|
|
26
|
+
types: Uint8Array | null,
|
|
27
|
+
data: Uint8Array | null,
|
|
28
|
+
masks: Uint8Array | null,
|
|
29
|
+
columnCount: bigint,
|
|
30
|
+
rowCount: bigint,
|
|
31
|
+
) => number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Marshal a column-oriented payload and forward it to one of the columnar group update C
|
|
35
|
+
* functions. Shared by updateTimeSeriesGroup / updateVectorGroup / updateSetGroup: the three
|
|
36
|
+
* differ only in which C entry point they call.
|
|
37
|
+
*
|
|
38
|
+
* Pass `{}` (no columns) to clear the group.
|
|
39
|
+
*/
|
|
40
|
+
export function updateGroupColumns(
|
|
41
|
+
handle: NativePointer,
|
|
42
|
+
caller: string,
|
|
43
|
+
update: ColumnUpdateFn,
|
|
44
|
+
collection: string,
|
|
45
|
+
group: string,
|
|
46
|
+
id: number,
|
|
47
|
+
data: GroupColumns,
|
|
48
|
+
): void {
|
|
49
|
+
const collBuf = toCString(collection);
|
|
50
|
+
const grpBuf = toCString(group);
|
|
51
|
+
const entries = Object.entries(data);
|
|
52
|
+
|
|
53
|
+
if (entries.length === 0) {
|
|
54
|
+
check(update(handle, collBuf.buf, grpBuf.buf, BigInt(id), null, null, null, null, 0n, 0n));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const columnCount = entries.length;
|
|
59
|
+
const rowCount = entries[0][1].length;
|
|
60
|
+
|
|
61
|
+
// Validate before marshalling: a jagged column would desync the parallel arrays the C API
|
|
62
|
+
// reads against row_count, and a zero-length column (with columns present) would otherwise
|
|
63
|
+
// marshal a null data pointer. Named-but-empty columns are a caller mistake -- pass {} to
|
|
64
|
+
// clear the group instead. The C API rejects both too; failing here names the column.
|
|
65
|
+
for (const [name, values] of entries) {
|
|
66
|
+
if (values.length !== rowCount) {
|
|
67
|
+
throw new QuiverError(
|
|
68
|
+
`Cannot ${caller}: column '${name}' has length ${values.length} but expected ${rowCount}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (rowCount === 0) {
|
|
73
|
+
const names = entries.map(([name]) => name).join(", ");
|
|
74
|
+
throw new QuiverError(
|
|
75
|
+
`Cannot ${caller}: columns [${names}] contain no rows; pass {} to clear the group`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const keepalive: Allocation[] = [];
|
|
80
|
+
|
|
81
|
+
// Build column names as native string array
|
|
82
|
+
const colNames = entries.map(([name]) => name);
|
|
83
|
+
const { table: namesTable, keepalive: namesPtrs } = allocNativeStringArray(colNames);
|
|
84
|
+
keepalive.push(namesTable, ...namesPtrs);
|
|
85
|
+
|
|
86
|
+
// Build column types, data, and per-cell NULL masks. A null cell becomes mask 0 + a
|
|
87
|
+
// placeholder in the data array (the C API never reads it). An all-null column is tagged
|
|
88
|
+
// FLOAT with zeroed data — the type tag is ignored for masked-out cells.
|
|
89
|
+
const typesBuf = new Uint8Array(columnCount * 4);
|
|
90
|
+
const typesDv = new DataView(typesBuf.buffer);
|
|
91
|
+
const dataPtrs: (Pointer | null)[] = [];
|
|
92
|
+
const maskPtrs: (Pointer | null)[] = [];
|
|
93
|
+
|
|
94
|
+
for (let c = 0; c < columnCount; c++) {
|
|
95
|
+
const [colName, values] = entries[c];
|
|
96
|
+
const first = values.find((v) => v !== null);
|
|
97
|
+
|
|
98
|
+
// Mask via direct indexing — never a DataView, to avoid the documented
|
|
99
|
+
// .buffer-materialization pitfall between ptr() and the FFI call.
|
|
100
|
+
const maskBuf = new Uint8Array(rowCount);
|
|
101
|
+
for (let r = 0; r < rowCount; r++) maskBuf[r] = values[r] === null ? 0 : 1;
|
|
102
|
+
const maskAlloc: Allocation = { ptr: ptr(maskBuf), buf: maskBuf };
|
|
103
|
+
keepalive.push(maskAlloc);
|
|
104
|
+
maskPtrs.push(maskAlloc.ptr);
|
|
105
|
+
|
|
106
|
+
if (first === undefined) {
|
|
107
|
+
// All-null column
|
|
108
|
+
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
109
|
+
const p = allocNativeFloat64(new Array(rowCount).fill(0));
|
|
110
|
+
keepalive.push(p);
|
|
111
|
+
dataPtrs.push(p.ptr);
|
|
112
|
+
} else if (typeof first === "string") {
|
|
113
|
+
typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
|
|
114
|
+
const { table, keepalive: strPtrs } = allocNativeStringArray(
|
|
115
|
+
values.map((v) => (v === null ? null : (v as string))),
|
|
116
|
+
);
|
|
117
|
+
keepalive.push(table, ...strPtrs);
|
|
118
|
+
dataPtrs.push(table.ptr);
|
|
119
|
+
} else if (typeof first === "number") {
|
|
120
|
+
const nonNull = values.filter((v) => v !== null) as number[];
|
|
121
|
+
const sanitized = values.map((v) => (v === null ? 0 : (v as number)));
|
|
122
|
+
if (nonNull.every((v) => Number.isInteger(v))) {
|
|
123
|
+
typesDv.setInt32(c * 4, DATA_TYPE_INTEGER, true);
|
|
124
|
+
const p = allocNativeInt64(sanitized);
|
|
125
|
+
keepalive.push(p);
|
|
126
|
+
dataPtrs.push(p.ptr);
|
|
127
|
+
} else {
|
|
128
|
+
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
129
|
+
const p = allocNativeFloat64(sanitized);
|
|
130
|
+
keepalive.push(p);
|
|
131
|
+
dataPtrs.push(p.ptr);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
throw new QuiverError(
|
|
135
|
+
`Cannot ${caller}: column '${colName}' has unsupported value type ${typeof first}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const typesAlloc: Allocation = { ptr: ptr(typesBuf), buf: typesBuf };
|
|
141
|
+
keepalive.push(typesAlloc);
|
|
142
|
+
const dataTable = allocNativePtrTable(dataPtrs);
|
|
143
|
+
keepalive.push(dataTable);
|
|
144
|
+
const maskTable = allocNativePtrTable(maskPtrs);
|
|
145
|
+
keepalive.push(maskTable);
|
|
146
|
+
|
|
147
|
+
check(
|
|
148
|
+
update(
|
|
149
|
+
handle,
|
|
150
|
+
collBuf.buf,
|
|
151
|
+
grpBuf.buf,
|
|
152
|
+
BigInt(id),
|
|
153
|
+
namesTable.buf,
|
|
154
|
+
typesAlloc.buf,
|
|
155
|
+
dataTable.buf,
|
|
156
|
+
maskTable.buf,
|
|
157
|
+
BigInt(columnCount),
|
|
158
|
+
BigInt(rowCount),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ 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 type { GroupColumns } from "./group-columns.ts";
|
|
14
15
|
export { LUA_DB_API_REFERENCE } from "./lua-api.ts";
|
|
15
16
|
export { LuaRunner } from "./lua-runner.ts";
|
|
16
17
|
export type { GroupMetadata, ScalarMetadata } from "./metadata.ts";
|
package/src/loader.ts
CHANGED
|
@@ -81,6 +81,7 @@ const readSymbols = {
|
|
|
81
81
|
quiver_database_read_set_floats_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
|
|
82
82
|
quiver_database_read_set_strings_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
|
|
83
83
|
quiver_database_read_element_ids: { args: [P, BUF, P, P], returns: I32 },
|
|
84
|
+
quiver_database_number_of_elements: { args: [P, BUF, P], returns: I32 },
|
|
84
85
|
} as const;
|
|
85
86
|
|
|
86
87
|
const querySymbols = {
|
|
@@ -97,6 +98,9 @@ const transactionSymbols = {
|
|
|
97
98
|
quiver_database_commit: { args: [P], returns: I32 },
|
|
98
99
|
quiver_database_rollback: { args: [P], returns: I32 },
|
|
99
100
|
quiver_database_in_transaction: { args: [P, P], returns: I32 },
|
|
101
|
+
quiver_database_begin_dry_run: { args: [P], returns: I32 },
|
|
102
|
+
quiver_database_end_dry_run: { args: [P], returns: I32 },
|
|
103
|
+
quiver_database_in_dry_run: { args: [P, P], returns: I32 },
|
|
100
104
|
} as const;
|
|
101
105
|
|
|
102
106
|
const metadataSymbols = {
|
|
@@ -131,6 +135,14 @@ const timeSeriesSymbols = {
|
|
|
131
135
|
args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
|
|
132
136
|
returns: I32,
|
|
133
137
|
},
|
|
138
|
+
quiver_database_update_vector_group: {
|
|
139
|
+
args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
|
|
140
|
+
returns: I32,
|
|
141
|
+
},
|
|
142
|
+
quiver_database_update_set_group: {
|
|
143
|
+
args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
|
|
144
|
+
returns: I32,
|
|
145
|
+
},
|
|
134
146
|
quiver_database_free_time_series_data: { args: [P, P, P, P, USIZE, USIZE], returns: I32 },
|
|
135
147
|
quiver_database_has_time_series_files: { args: [P, BUF, P], returns: I32 },
|
|
136
148
|
quiver_database_list_time_series_files_columns: { args: [P, BUF, P, P], returns: I32 },
|
|
@@ -160,7 +172,8 @@ const freeSymbols = {
|
|
|
160
172
|
const luaSymbols = {
|
|
161
173
|
quiver_lua_runner_new: { args: [P, P], returns: I32 },
|
|
162
174
|
quiver_lua_runner_free: { args: [P], returns: I32 },
|
|
163
|
-
quiver_lua_runner_run: { args: [P, BUF], returns: I32 },
|
|
175
|
+
quiver_lua_runner_run: { args: [P, BUF, BUF], returns: I32 },
|
|
176
|
+
quiver_lua_runner_free_string: { args: [P], returns: I32 },
|
|
164
177
|
} as const;
|
|
165
178
|
|
|
166
179
|
// 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.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
13
|
-
// `db:<snake_case_name
|
|
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.**
|
|
70
|
-
|
|
71
|
-
\`loadfile\` are removed (string-form \`load\`
|
|
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.**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
@@ -92,6 +129,7 @@ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Py
|
|
|
92
129
|
db:is_healthy() -- boolean
|
|
93
130
|
db:current_version() -- integer (current migration version)
|
|
94
131
|
db:path() -- string (database file path)
|
|
132
|
+
db:number_of_elements(collection) -- integer: how many elements the collection holds right now
|
|
95
133
|
db:describe() -- string: whole-DB text report (returns it, does NOT print)
|
|
96
134
|
db:describe_collection(collection) -- string: one collection's structure (text report)
|
|
97
135
|
db:summarize_collection(collection)-- string: per-scalar null/non-null counts, low-cardinality
|
|
@@ -121,14 +159,54 @@ db:transaction(function(db)
|
|
|
121
159
|
end) -- both writes commit together; if either throws, both roll back
|
|
122
160
|
\`\`\`
|
|
123
161
|
|
|
124
|
-
**Caveat:** if the host already runs your script inside a transaction, an
|
|
125
|
-
\`db:begin_transaction()\` will error (\`cannot start a transaction within a transaction\`)
|
|
126
|
-
mid-script \`db:commit()\` would prematurely end the host's transaction. When unsure whether
|
|
127
|
-
transaction is already open, check \`db:in_transaction()\` first, or just issue writes directly —
|
|
162
|
+
**Caveat:** if the host already runs your script inside a plain transaction (not a dry run), an
|
|
163
|
+
explicit \`db:begin_transaction()\` will error (\`cannot start a transaction within a transaction\`)
|
|
164
|
+
and a mid-script \`db:commit()\` would prematurely end the host's transaction. When unsure whether
|
|
165
|
+
a transaction is already open, check \`db:in_transaction()\` first, or just issue writes directly —
|
|
128
166
|
each \`db:\` write is durable on its own.
|
|
129
167
|
|
|
130
168
|
---
|
|
131
169
|
|
|
170
|
+
## Dry runs
|
|
171
|
+
|
|
172
|
+
A dry run executes writes and then throws them away. Use it to check that a sequence works — that
|
|
173
|
+
the collections exist, the types match, the foreign keys resolve — before committing to it.
|
|
174
|
+
|
|
175
|
+
\`\`\`lua
|
|
176
|
+
db:dry_run(fn) -- run fn(db), roll everything back, return fn's result
|
|
177
|
+
db:begin_dry_run() -- start one explicitly
|
|
178
|
+
db:end_dry_run() -- end it, rolling back everything it covered
|
|
179
|
+
db:in_dry_run() -- boolean: is a dry run currently active?
|
|
180
|
+
\`\`\`
|
|
181
|
+
|
|
182
|
+
\`\`\`lua
|
|
183
|
+
local preview = db:dry_run(function(db)
|
|
184
|
+
db:create_element("Collection", { label = "Item 1", some_integer = 42 })
|
|
185
|
+
return db:read_element_ids("Collection") -- reads see the uncommitted writes
|
|
186
|
+
end)
|
|
187
|
+
-- nothing was kept; preview holds what the reads saw
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
Rules worth knowing:
|
|
191
|
+
|
|
192
|
+
- **Nested transaction control is absorbed.** Inside a dry run, \`db:begin_transaction\`,
|
|
193
|
+
\`db:commit\` and \`db:rollback\` become no-ops, so the \`db:transaction\` pattern above composes
|
|
194
|
+
instead of erroring — \`db:transaction(fn)\` still runs \`fn\` and still performs its writes, only
|
|
195
|
+
its BEGIN/COMMIT are absorbed. The flip side: a nested \`db:rollback\` does **not** partially
|
|
196
|
+
undo — everything is undone when the dry run ends, whatever the nested calls asked for.
|
|
197
|
+
- \`db:in_transaction()\` still reports \`true\` during a dry run: a real transaction is open.
|
|
198
|
+
- \`db:import_csv\` cannot run inside a dry run (it toggles a pragma that is a no-op mid-transaction)
|
|
199
|
+
and throws \`Cannot import_csv: transaction already active\`.
|
|
200
|
+
- **Dry runs do not nest.** The host may have already opened one around your whole script, in which
|
|
201
|
+
case both \`db:begin_dry_run()\` and \`db:dry_run(fn)\` (which calls it internally) error with
|
|
202
|
+
\`Cannot begin_dry_run: dry run already active\` — check \`db:in_dry_run()\` first and skip the
|
|
203
|
+
wrapper when one is already active. Do **not** call \`db:end_dry_run()\` to get around it: that
|
|
204
|
+
ends the host's dry run, and everything you write afterwards is committed for real.
|
|
205
|
+
- For a rough sense of how much a run touched, \`db:query_integer("SELECT total_changes()")\` gives
|
|
206
|
+
the number of rows inserted, updated or deleted on this connection.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
132
210
|
## CRUD
|
|
133
211
|
|
|
134
212
|
\`\`\`lua
|
|
@@ -164,8 +242,9 @@ Notes:
|
|
|
164
242
|
type can't be inferred from an empty array), so it is silently dropped.
|
|
165
243
|
- **No \`nil\` scalar attributes.** In Lua a key set to \`nil\` is dropped from the table, so
|
|
166
244
|
\`{ x = nil }\` is identical to \`{}\`; an update/create table that ends up with no attributes
|
|
167
|
-
**throws** (
|
|
168
|
-
|
|
245
|
+
**throws** (\`...must have at least one scalar attribute\` on create, \`...at least one attribute
|
|
246
|
+
to update\` on update). To leave a column unchanged, omit the key — you cannot set a scalar to
|
|
247
|
+
NULL via the element table. (\`nil\` → NULL is only accepted by
|
|
169
248
|
\`upsert_time_series_row\` and \`update_time_series_files\`.)
|
|
170
249
|
|
|
171
250
|
---
|
|
@@ -206,6 +285,36 @@ db:read_set_strings(collection, attribute)
|
|
|
206
285
|
|
|
207
286
|
---
|
|
208
287
|
|
|
288
|
+
## Replace a whole vector or set group (column-oriented)
|
|
289
|
+
|
|
290
|
+
\`update_vector_group\` / \`update_set_group\` replace **all** of one element's rows in one *named*
|
|
291
|
+
group, taking the same column-oriented shape as the time-series writer:
|
|
292
|
+
|
|
293
|
+
\`\`\`lua
|
|
294
|
+
db:update_vector_group("Child", "refs", id, { parent_ref = { 1, 2, 3 } })
|
|
295
|
+
db:update_set_group("Child", "parents", id, { parent_ref = { 1, 2 } })
|
|
296
|
+
|
|
297
|
+
db:update_vector_group("Child", "refs", id, {}) -- clears the group
|
|
298
|
+
\`\`\`
|
|
299
|
+
|
|
300
|
+
Use these instead of routing a group's columns through \`update_element\` whenever a column name is
|
|
301
|
+
shared by two groups of the collection (legal for foreign-key columns): \`update_element\` routes an
|
|
302
|
+
array **by column name**, so it writes to *every* group table that has that column — silently
|
|
303
|
+
rewriting groups you never named. \`(collection, group)\` names exactly one table.
|
|
304
|
+
|
|
305
|
+
Rules:
|
|
306
|
+
- **Row count is the largest index any column reaches.** Shorter or sparse columns write NULL in
|
|
307
|
+
the gaps, so \`nil\` holes from a read round-trip.
|
|
308
|
+
- **\`{}\` (no columns) clears the group.** Naming a column whose array is empty is an error, not a
|
|
309
|
+
clear — a typo'd column name must not destroy data.
|
|
310
|
+
- **\`id\` and \`vector_index\` are managed by the group** (the element and the row's position) and
|
|
311
|
+
are rejected if passed.
|
|
312
|
+
- **Foreign-key columns accept a label string** and resolve it to the referenced id, exactly as in
|
|
313
|
+
\`create_element\` / \`update_element\`.
|
|
314
|
+
- The element id must exist, same as \`update_element\` / \`delete_element\`.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
209
318
|
## Composite by-id reads (Lua convenience helpers)
|
|
210
319
|
|
|
211
320
|
\`\`\`lua
|
|
@@ -489,7 +598,8 @@ db:csv_to_bin(path)
|
|
|
489
598
|
|
|
490
599
|
local e = (quiver.expression(r) + 10.0) * 2.0 -- files auto-wrap; scalars either side
|
|
491
600
|
e = quiver.abs(e); e = quiver.sqrt(e) -- also quiver.log / quiver.exp
|
|
492
|
-
local cond_e = quiver.gt(e, 3.0) --
|
|
601
|
+
local cond_e = quiver.gt(e, 3.0) -- also quiver.lt/quiver.gte/quiver.lte/quiver.eq/quiver.neq
|
|
602
|
+
-- all -> 1.0/0.0 per element (NaN operand -> NaN)
|
|
493
603
|
cond_e = cond_e & ~quiver.lt(e, 1.0) -- boolean logic via & | ~ operators (and/or/not are keywords)
|
|
494
604
|
e = quiver.ifelse(cond_e, then_e, else_e) -- build cond_e with comparison + logical operators
|
|
495
605
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/read.ts
CHANGED
|
@@ -211,6 +211,14 @@ Database.prototype.readElementIds = function (this: Database, collection: string
|
|
|
211
211
|
return result;
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
Database.prototype.numberOfElements = function (this: Database, collection: string): number {
|
|
215
|
+
const lib = getSymbols();
|
|
216
|
+
const collBuf = toCString(collection);
|
|
217
|
+
const outBuf = new Uint8Array(8);
|
|
218
|
+
check(lib.quiver_database_number_of_elements(this._handle, collBuf.buf, outBuf));
|
|
219
|
+
return Number(new DataView(outBuf.buffer).getBigInt64(0, true));
|
|
220
|
+
};
|
|
221
|
+
|
|
214
222
|
// --- Vector bulk reads ---
|
|
215
223
|
|
|
216
224
|
function readBulkIntegers(
|
package/src/time-series.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
readUint64Out,
|
|
18
18
|
toCString,
|
|
19
19
|
} from "./ffi-helpers.ts";
|
|
20
|
+
import { updateGroupColumns } from "./group-columns.ts";
|
|
20
21
|
import { getSymbols } from "./loader.ts";
|
|
21
22
|
import {
|
|
22
23
|
type Allocation,
|
|
@@ -188,133 +189,14 @@ Database.prototype.updateTimeSeriesGroup = function (
|
|
|
188
189
|
id: number,
|
|
189
190
|
data: TimeSeriesData,
|
|
190
191
|
): void {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this._handle,
|
|
200
|
-
collBuf.buf,
|
|
201
|
-
grpBuf.buf,
|
|
202
|
-
BigInt(id),
|
|
203
|
-
null,
|
|
204
|
-
null,
|
|
205
|
-
null,
|
|
206
|
-
null,
|
|
207
|
-
0n,
|
|
208
|
-
0n,
|
|
209
|
-
),
|
|
210
|
-
);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const columnCount = entries.length;
|
|
215
|
-
const rowCount = entries[0][1].length;
|
|
216
|
-
|
|
217
|
-
// Validate before marshalling: a jagged column would desync the parallel
|
|
218
|
-
// arrays the C API reads against row_count, and a zero-length column (with
|
|
219
|
-
// columns present) would otherwise marshal a null data pointer that the C API
|
|
220
|
-
// dereferences. Named-but-empty columns are a caller mistake -- pass {} to
|
|
221
|
-
// clear the group instead.
|
|
222
|
-
for (const [name, values] of entries) {
|
|
223
|
-
if (values.length !== rowCount) {
|
|
224
|
-
throw new QuiverError(
|
|
225
|
-
`Cannot updateTimeSeriesGroup: column '${name}' has length ${values.length} but expected ${rowCount}`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (rowCount === 0) {
|
|
230
|
-
const names = entries.map(([name]) => name).join(", ");
|
|
231
|
-
throw new QuiverError(
|
|
232
|
-
`Cannot updateTimeSeriesGroup: columns [${names}] contain no rows; pass {} to clear the group`,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const keepalive: Allocation[] = [];
|
|
237
|
-
|
|
238
|
-
// Build column names as native string array
|
|
239
|
-
const colNames = entries.map(([name]) => name);
|
|
240
|
-
const { table: namesTable, keepalive: namesPtrs } = allocNativeStringArray(colNames);
|
|
241
|
-
keepalive.push(namesTable, ...namesPtrs);
|
|
242
|
-
|
|
243
|
-
// Build column types, data, and per-cell NULL masks. A null cell becomes
|
|
244
|
-
// mask 0 + a placeholder in the data array (the C API never reads it). An
|
|
245
|
-
// all-null column is tagged FLOAT with zeroed data — the type tag is ignored
|
|
246
|
-
// for masked-out cells.
|
|
247
|
-
const typesBuf = new Uint8Array(columnCount * 4);
|
|
248
|
-
const typesDv = new DataView(typesBuf.buffer);
|
|
249
|
-
const dataPtrs: (Pointer | null)[] = [];
|
|
250
|
-
const maskPtrs: (Pointer | null)[] = [];
|
|
251
|
-
|
|
252
|
-
for (let c = 0; c < columnCount; c++) {
|
|
253
|
-
const [colName, values] = entries[c];
|
|
254
|
-
const first = values.find((v) => v !== null);
|
|
255
|
-
|
|
256
|
-
// Mask via direct indexing — never a DataView, to avoid the documented
|
|
257
|
-
// .buffer-materialization pitfall between ptr() and the FFI call.
|
|
258
|
-
const maskBuf = new Uint8Array(rowCount);
|
|
259
|
-
for (let r = 0; r < rowCount; r++) maskBuf[r] = values[r] === null ? 0 : 1;
|
|
260
|
-
const maskAlloc: Allocation = { ptr: ptr(maskBuf), buf: maskBuf };
|
|
261
|
-
keepalive.push(maskAlloc);
|
|
262
|
-
maskPtrs.push(maskAlloc.ptr);
|
|
263
|
-
|
|
264
|
-
if (first === undefined) {
|
|
265
|
-
// All-null column
|
|
266
|
-
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
267
|
-
const p = allocNativeFloat64(new Array(rowCount).fill(0));
|
|
268
|
-
keepalive.push(p);
|
|
269
|
-
dataPtrs.push(p.ptr);
|
|
270
|
-
} else if (typeof first === "string") {
|
|
271
|
-
typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
|
|
272
|
-
const { table, keepalive: strPtrs } = allocNativeStringArray(
|
|
273
|
-
values.map((v) => (v === null ? null : (v as string))),
|
|
274
|
-
);
|
|
275
|
-
keepalive.push(table, ...strPtrs);
|
|
276
|
-
dataPtrs.push(table.ptr);
|
|
277
|
-
} else if (typeof first === "number") {
|
|
278
|
-
const nonNull = values.filter((v) => v !== null) as number[];
|
|
279
|
-
const sanitized = values.map((v) => (v === null ? 0 : (v as number)));
|
|
280
|
-
if (nonNull.every((v) => Number.isInteger(v))) {
|
|
281
|
-
typesDv.setInt32(c * 4, DATA_TYPE_INTEGER, true);
|
|
282
|
-
const p = allocNativeInt64(sanitized);
|
|
283
|
-
keepalive.push(p);
|
|
284
|
-
dataPtrs.push(p.ptr);
|
|
285
|
-
} else {
|
|
286
|
-
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
287
|
-
const p = allocNativeFloat64(sanitized);
|
|
288
|
-
keepalive.push(p);
|
|
289
|
-
dataPtrs.push(p.ptr);
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
throw new QuiverError(
|
|
293
|
-
`Cannot updateTimeSeriesGroup: column '${colName}' has unsupported value type ${typeof first}`,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const typesAlloc: Allocation = { ptr: ptr(typesBuf), buf: typesBuf };
|
|
299
|
-
keepalive.push(typesAlloc);
|
|
300
|
-
const dataTable = allocNativePtrTable(dataPtrs);
|
|
301
|
-
keepalive.push(dataTable);
|
|
302
|
-
const maskTable = allocNativePtrTable(maskPtrs);
|
|
303
|
-
keepalive.push(maskTable);
|
|
304
|
-
|
|
305
|
-
check(
|
|
306
|
-
lib.quiver_database_update_time_series_group(
|
|
307
|
-
this._handle,
|
|
308
|
-
collBuf.buf,
|
|
309
|
-
grpBuf.buf,
|
|
310
|
-
BigInt(id),
|
|
311
|
-
namesTable.buf,
|
|
312
|
-
typesAlloc.buf,
|
|
313
|
-
dataTable.buf,
|
|
314
|
-
maskTable.buf,
|
|
315
|
-
BigInt(columnCount),
|
|
316
|
-
BigInt(rowCount),
|
|
317
|
-
),
|
|
192
|
+
updateGroupColumns(
|
|
193
|
+
this._handle,
|
|
194
|
+
"updateTimeSeriesGroup",
|
|
195
|
+
getSymbols().quiver_database_update_time_series_group,
|
|
196
|
+
collection,
|
|
197
|
+
group,
|
|
198
|
+
id,
|
|
199
|
+
data,
|
|
318
200
|
);
|
|
319
201
|
};
|
|
320
202
|
|
package/src/transaction.ts
CHANGED
|
@@ -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
|
+
};
|