quiverdb 0.9.16 → 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 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
 
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.9.16",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,4 +31,4 @@
31
31
  "@biomejs/biome": "^2.4.6",
32
32
  "@types/bun": "latest"
33
33
  }
34
- }
34
+ }
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(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
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(lib.quiver_element_set_array_string(elemPtr, nameBuf.buf, table.buf, values.length, null));
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(
@@ -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
- const lib = getSymbols();
192
- const collBuf = toCString(collection);
193
- const grpBuf = toCString(group);
194
- const entries = Object.entries(data);
195
-
196
- if (entries.length === 0) {
197
- check(
198
- lib.quiver_database_update_time_series_group(
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