quiverdb 0.9.16 → 0.10.1
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 +1 -0
- 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 +14 -0
- package/src/group-columns.ts +161 -0
- package/src/index.ts +1 -0
- package/src/loader.ts +9 -0
- package/src/lua-api.ts +31 -0
- package/src/read.ts +8 -0
- package/src/time-series.ts +9 -127
package/README.md
CHANGED
|
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[][];
|
|
@@ -154,6 +156,18 @@ export class Database {
|
|
|
154
156
|
id: number,
|
|
155
157
|
data: TimeSeriesData,
|
|
156
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;
|
|
157
171
|
declare upsertTimeSeriesRow: (
|
|
158
172
|
collection: string,
|
|
159
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 = {
|
|
@@ -134,6 +135,14 @@ const timeSeriesSymbols = {
|
|
|
134
135
|
args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
|
|
135
136
|
returns: I32,
|
|
136
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
|
+
},
|
|
137
146
|
quiver_database_free_time_series_data: { args: [P, P, P, P, USIZE, USIZE], returns: I32 },
|
|
138
147
|
quiver_database_has_time_series_files: { args: [P, BUF, P], returns: I32 },
|
|
139
148
|
quiver_database_list_time_series_files_columns: { args: [P, BUF, P, P], returns: I32 },
|
package/src/lua-api.ts
CHANGED
|
@@ -129,6 +129,7 @@ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Py
|
|
|
129
129
|
db:is_healthy() -- boolean
|
|
130
130
|
db:current_version() -- integer (current migration version)
|
|
131
131
|
db:path() -- string (database file path)
|
|
132
|
+
db:number_of_elements(collection) -- integer: how many elements the collection holds right now
|
|
132
133
|
db:describe() -- string: whole-DB text report (returns it, does NOT print)
|
|
133
134
|
db:describe_collection(collection) -- string: one collection's structure (text report)
|
|
134
135
|
db:summarize_collection(collection)-- string: per-scalar null/non-null counts, low-cardinality
|
|
@@ -284,6 +285,36 @@ db:read_set_strings(collection, attribute)
|
|
|
284
285
|
|
|
285
286
|
---
|
|
286
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
|
+
|
|
287
318
|
## Composite by-id reads (Lua convenience helpers)
|
|
288
319
|
|
|
289
320
|
\`\`\`lua
|
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
|
|