quiverdb 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/composites.ts CHANGED
@@ -39,9 +39,9 @@ Database.prototype.readVectorsById = function (
39
39
  this: Database,
40
40
  collection: string,
41
41
  id: number,
42
- ): Record<string, number[] | string[]> {
42
+ ): Record<string, (number | null)[] | (string | null)[]> {
43
43
  const groups = this.listVectorGroups(collection);
44
- const result: Record<string, number[] | string[]> = {};
44
+ const result: Record<string, (number | null)[] | (string | null)[]> = {};
45
45
 
46
46
  for (const group of groups) {
47
47
  for (const col of group.valueColumns) {
@@ -69,9 +69,9 @@ Database.prototype.readSetsById = function (
69
69
  this: Database,
70
70
  collection: string,
71
71
  id: number,
72
- ): Record<string, number[] | string[]> {
72
+ ): Record<string, (number | null)[] | (string | null)[]> {
73
73
  const groups = this.listSetGroups(collection);
74
- const result: Record<string, number[] | string[]> = {};
74
+ const result: Record<string, (number | null)[] | (string | null)[]> = {};
75
75
 
76
76
  for (const group of groups) {
77
77
  for (const col of group.valueColumns) {
package/src/create.ts CHANGED
@@ -29,6 +29,15 @@ function setElementArray(
29
29
 
30
30
  const first = values[0];
31
31
 
32
+ // A vector/set read returns a NULL cell as null. The element surface is non-null (NULL cells go
33
+ // through updateVectorGroup / updateSetGroup), and the numeric setters below would store a null
34
+ // as 0 / false. A string array keeps its NULL-pointer path, which the C setter reads as NULL.
35
+ if (typeof first !== "string" && values.some((v) => v === null || v === undefined)) {
36
+ throw new QuiverError(
37
+ `Unsupported null cell in array '${name}': write NULL cells with updateVectorGroup or updateSetGroup`,
38
+ );
39
+ }
40
+
32
41
  if (typeof first === "bigint") {
33
42
  const arr = allocNativeInt64(values as bigint[]);
34
43
  check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
package/src/database.ts CHANGED
@@ -138,22 +138,54 @@ export class Database {
138
138
  ) => string | null;
139
139
  declare readElementIds: (collection: string) => number[];
140
140
  declare numberOfElements: (collection: string) => number;
141
- declare readVectorIntegers: (collection: string, attribute: string) => number[][];
142
- declare readVectorBooleans: (collection: string, attribute: string) => boolean[][];
143
- declare readVectorFloats: (collection: string, attribute: string) => number[][];
144
- declare readVectorStrings: (collection: string, attribute: string) => string[][];
145
- declare readVectorIntegersById: (collection: string, attribute: string, id: number) => number[];
146
- declare readVectorBooleansById: (collection: string, attribute: string, id: number) => boolean[];
147
- declare readVectorFloatsById: (collection: string, attribute: string, id: number) => number[];
148
- declare readVectorStringsById: (collection: string, attribute: string, id: number) => string[];
149
- declare readSetIntegers: (collection: string, attribute: string) => number[][];
150
- declare readSetBooleans: (collection: string, attribute: string) => boolean[][];
151
- declare readSetFloats: (collection: string, attribute: string) => number[][];
152
- declare readSetStrings: (collection: string, attribute: string) => string[][];
153
- declare readSetIntegersById: (collection: string, attribute: string, id: number) => number[];
154
- declare readSetBooleansById: (collection: string, attribute: string, id: number) => boolean[];
155
- declare readSetFloatsById: (collection: string, attribute: string, id: number) => number[];
156
- declare readSetStringsById: (collection: string, attribute: string, id: number) => string[];
141
+ declare readVectorIntegers: (collection: string, attribute: string) => (number | null)[][];
142
+ declare readVectorBooleans: (collection: string, attribute: string) => (boolean | null)[][];
143
+ declare readVectorFloats: (collection: string, attribute: string) => (number | null)[][];
144
+ declare readVectorStrings: (collection: string, attribute: string) => (string | null)[][];
145
+ declare readVectorIntegersById: (
146
+ collection: string,
147
+ attribute: string,
148
+ id: number,
149
+ ) => (number | null)[];
150
+ declare readVectorBooleansById: (
151
+ collection: string,
152
+ attribute: string,
153
+ id: number,
154
+ ) => (boolean | null)[];
155
+ declare readVectorFloatsById: (
156
+ collection: string,
157
+ attribute: string,
158
+ id: number,
159
+ ) => (number | null)[];
160
+ declare readVectorStringsById: (
161
+ collection: string,
162
+ attribute: string,
163
+ id: number,
164
+ ) => (string | null)[];
165
+ declare readSetIntegers: (collection: string, attribute: string) => (number | null)[][];
166
+ declare readSetBooleans: (collection: string, attribute: string) => (boolean | null)[][];
167
+ declare readSetFloats: (collection: string, attribute: string) => (number | null)[][];
168
+ declare readSetStrings: (collection: string, attribute: string) => (string | null)[][];
169
+ declare readSetIntegersById: (
170
+ collection: string,
171
+ attribute: string,
172
+ id: number,
173
+ ) => (number | null)[];
174
+ declare readSetBooleansById: (
175
+ collection: string,
176
+ attribute: string,
177
+ id: number,
178
+ ) => (boolean | null)[];
179
+ declare readSetFloatsById: (
180
+ collection: string,
181
+ attribute: string,
182
+ id: number,
183
+ ) => (number | null)[];
184
+ declare readSetStringsById: (
185
+ collection: string,
186
+ attribute: string,
187
+ id: number,
188
+ ) => (string | null)[];
157
189
 
158
190
  // --- Queries (implemented in query.ts) ---
159
191
  declare queryString: (sql: string, parameters?: QueryParam[]) => string | null;
@@ -268,6 +300,12 @@ export class Database {
268
300
  collection: string,
269
301
  id: number,
270
302
  ) => Record<string, number | string | null>;
271
- declare readVectorsById: (collection: string, id: number) => Record<string, number[] | string[]>;
272
- declare readSetsById: (collection: string, id: number) => Record<string, number[] | string[]>;
303
+ declare readVectorsById: (
304
+ collection: string,
305
+ id: number,
306
+ ) => Record<string, (number | null)[] | (string | null)[]>;
307
+ declare readSetsById: (
308
+ collection: string,
309
+ id: number,
310
+ ) => Record<string, (number | null)[] | (string | null)[]>;
273
311
  }
package/src/loader.ts CHANGED
@@ -73,17 +73,17 @@ const readSymbols = {
73
73
  quiver_database_read_scalar_integer_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
74
74
  quiver_database_read_scalar_float_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
75
75
  quiver_database_read_scalar_string_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
76
- quiver_database_read_vector_integers: { args: [P, BUF, BUF, P, P, P], returns: I32 },
77
- quiver_database_read_vector_floats: { args: [P, BUF, BUF, P, P, P], returns: I32 },
76
+ quiver_database_read_vector_integers: { args: [P, BUF, BUF, P, P, P, P], returns: I32 },
77
+ quiver_database_read_vector_floats: { args: [P, BUF, BUF, P, P, P, P], returns: I32 },
78
78
  quiver_database_read_vector_strings: { args: [P, BUF, BUF, P, P, P], returns: I32 },
79
- quiver_database_read_set_integers: { args: [P, BUF, BUF, P, P, P], returns: I32 },
80
- quiver_database_read_set_floats: { args: [P, BUF, BUF, P, P, P], returns: I32 },
79
+ quiver_database_read_set_integers: { args: [P, BUF, BUF, P, P, P, P], returns: I32 },
80
+ quiver_database_read_set_floats: { args: [P, BUF, BUF, P, P, P, P], returns: I32 },
81
81
  quiver_database_read_set_strings: { args: [P, BUF, BUF, P, P, P], returns: I32 },
82
- quiver_database_read_vector_integers_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
83
- quiver_database_read_vector_floats_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
82
+ quiver_database_read_vector_integers_by_id: { args: [P, BUF, BUF, I64, P, P, P], returns: I32 },
83
+ quiver_database_read_vector_floats_by_id: { args: [P, BUF, BUF, I64, P, P, P], returns: I32 },
84
84
  quiver_database_read_vector_strings_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
85
- quiver_database_read_set_integers_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
86
- quiver_database_read_set_floats_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
85
+ quiver_database_read_set_integers_by_id: { args: [P, BUF, BUF, I64, P, P, P], returns: I32 },
86
+ quiver_database_read_set_floats_by_id: { args: [P, BUF, BUF, I64, P, P, P], returns: I32 },
87
87
  quiver_database_read_set_strings_by_id: { args: [P, BUF, BUF, I64, P, P], returns: I32 },
88
88
  quiver_database_read_element_ids: { args: [P, BUF, P, P], returns: I32 },
89
89
  quiver_database_number_of_elements: { args: [P, BUF, P], returns: I32 },
@@ -181,6 +181,7 @@ const freeSymbols = {
181
181
  quiver_database_free_integer_array: { args: [P], returns: I32 },
182
182
  quiver_database_free_float_array: { args: [P], returns: I32 },
183
183
  quiver_database_free_mask: { args: [P], returns: I32 },
184
+ quiver_database_free_masks: { args: [P, USIZE], returns: I32 },
184
185
  quiver_database_free_string_array: { args: [P, USIZE], returns: I32 },
185
186
  quiver_database_free_string: { args: [P], returns: I32 },
186
187
  quiver_database_free_integer_vectors: { args: [P, P, USIZE], returns: I32 },
package/src/lua-api.ts CHANGED
@@ -127,9 +127,14 @@ midnight.
127
127
 
128
128
  **A table with holes is an object, not an array.** A bulk read of a nullable column returns
129
129
  \`nil\` holes (see Reading), so \`return db:read_scalar_integers(c, a)\` encodes as
130
- \`{"1":10,"3":30}\` — not \`[10,null,30]\` — and the keys sort as text (\`"1","11","2"\`). When the host
131
- needs positional data, return the ids alongside and fill the holes yourself:
130
+ \`{"1":10,"3":30}\` — not \`[10,null,30]\` — and the keys sort as text (\`"1","11","2"\`). When the
131
+ host needs positional scalar data, return the ids alongside and fill the holes yourself:
132
132
  \`local v = db:read_scalar_integers(c, a); local out = {}; for i in ipairs(ids) do out[i] = v[i] or false end\`.
133
+ **Inside** a vector/set read an inner list with an interior NULL cell encodes as an object nested
134
+ in the outer array (\`[{"1":10,"3":30},[]]\`), but a trailing NULL cell leaves no trace at all:
135
+ \`[10, NULL]\` encodes as the plain \`[10]\`. The ids count elements, not rows, so they cannot
136
+ restore inner positions — take the row count from a \`NOT NULL\` column of the group (see
137
+ *Vector reads*), or do that read in the host binding.
133
138
 
134
139
  Returning a function, a coroutine, or a userdata (including \`db\` itself) raises
135
140
  \`Cannot run: script returned an unsupported Lua type\`; nesting deeper than 32 levels raises
@@ -280,6 +285,10 @@ Notes:
280
285
  \`Cannot update_element: ...\`.
281
286
  - **Empty arrays are skipped.** An attribute whose value is \`{}\` writes no vector/set (the element
282
287
  type can't be inferred from an empty array), so it is silently dropped.
288
+ - **Arrays must be dense.** A vector/set read returns a NULL cell as a \`nil\` hole, but an element
289
+ array cannot carry one: \`create_element\` / \`update_element\` throw \`array '<name>' has a nil
290
+ hole ...\` rather than cut the array short at the hole. Write NULL cells with
291
+ \`update_vector_group\` / \`update_set_group\`, which write a hole as NULL.
283
292
  - **No \`nil\` scalar attributes.** In Lua a key set to \`nil\` is dropped from the table, so
284
293
  \`{ x = nil }\` is identical to \`{}\`; an update/create table that ends up with no attributes
285
294
  **throws** (\`...must have at least one scalar attribute\` on create, \`...at least one attribute
@@ -314,10 +323,13 @@ db:read_scalar_strings(collection, attribute) -- { "Item 1", "Item 2", ... }
314
323
 
315
324
  ## Vector reads (bulk)
316
325
 
317
- Each returns an array of arrays — one inner array per element.
326
+ Each returns an array of arrays — one inner array per element, aligned with
327
+ \`db:read_element_ids\`; an element with no rows is \`{}\`. A NULL cell is a \`nil\` hole, so on a
328
+ nullable column \`#\` and \`ipairs\` are unreliable on an inner list, and a trailing NULL is
329
+ invisible (\`[10, NULL]\` reads as \`{10}\`; a NULL-only row reads as \`{}\`).
318
330
 
319
331
  \`\`\`lua
320
- db:read_vector_integers(collection, attribute) -- { {1,2,3}, {2,3,4}, ... }
332
+ db:read_vector_integers(collection, attribute) -- { {1,2,3}, {10,nil,30}, {}, ... }
321
333
  db:read_vector_floats(collection, attribute)
322
334
  db:read_vector_strings(collection, attribute)
323
335
  \`\`\`
@@ -326,7 +338,7 @@ db:read_vector_strings(collection, attribute)
326
338
 
327
339
  ## Set reads (bulk)
328
340
 
329
- Same shape as vector reads — an array of arrays.
341
+ Same shape and NULL handling as vector reads — an array of arrays with \`nil\` holes.
330
342
 
331
343
  \`\`\`lua
332
344
  db:read_set_integers(collection, attribute)
@@ -358,7 +370,8 @@ rewriting groups you never named. \`(collection, group)\` names exactly one tabl
358
370
 
359
371
  Rules:
360
372
  - **Row count is the largest index any column reaches.** Shorter or sparse columns write NULL in
361
- the gaps, so \`nil\` holes from a read round-trip.
373
+ the gaps, so interior \`nil\` holes from a read round-trip. A trailing row that is NULL in every
374
+ column is invisible to a read (see *Vector reads*), so writing a read back drops it.
362
375
  - **\`{}\` (no columns) clears the group.** Naming a column whose array is empty is an error, not a
363
376
  clear — a typo'd column name must not destroy data.
364
377
  - **\`id\` and \`vector_index\` are managed by the group** (the element and the row's position) and
@@ -376,20 +389,17 @@ Rules:
376
389
  db:read_element_ids(collection) -- { 1, 2, 3, ... }
377
390
 
378
391
  db:read_scalars_by_id(collection, id) -- { attr = value, ... } (missing -> nil)
379
- db:read_vectors_by_id(collection, id) -- { column = { v1, v2, ... }, ... }
380
- db:read_sets_by_id(collection, id) -- { column = { v1, v2, ... }, ... }
392
+ db:read_vectors_by_id(collection, id) -- { column = { v1, nil, v3, ... }, ... }
393
+ db:read_sets_by_id(collection, id) -- { column = { v1, nil, v3, ... }, ... }
381
394
  db:read_element_by_id(collection, id) -- scalars + vectors + sets merged into one table
382
395
  \`\`\`
383
396
 
384
397
  \`read_element_by_id\` merges every scalar, vector, and set for the element into a single table.
385
398
  Scalar attributes with no value come back as \`nil\`.
386
399
 
387
- **Group columns are returned densely, with NULL cells dropped.** \`read_vectors_by_id\` and
388
- \`read_sets_by_id\` read each column independently, and a column read skips its NULL cells — so two
389
- columns of the same group are **not** positionally aligned with each other whenever one is
390
- nullable (e.g. an \`ON DELETE SET NULL\` relation). Do not zip them into rows. There is no
391
- row-aligned group read in Lua; if you need per-row alignment across a nullable group, do that read
392
- in the host binding instead.
400
+ **Group columns keep NULL cells as \`nil\` holes**, so cell *i* of every column of one group is
401
+ the same row. Zipping them needs the row count: \`#\` of a \`NOT NULL\` column of the group.
402
+ There is no row-shaped group read in Lua.
393
403
 
394
404
  ---
395
405
 
@@ -856,6 +866,6 @@ and \`unit\` default to \`""\`; \`labels\`, \`dimensions\`, \`dimension_sizes\`,
856
866
  DateTime wrapper helpers (Lua uses ISO 8601 strings), boolean *reader* helpers (a stored flag reads
857
867
  back as \`0\`/\`1\` — writing a boolean is supported), \`_by_id\` single-scalar variants (use the
858
868
  composite by-id readers or the bulk readers instead), and the row-aligned whole-group readers the
859
- other bindings have (hence the null-dropping caveat under composite by-id reads). Everything else
869
+ other bindings have (hence the row-count caveat under composite by-id reads). Everything else
860
870
  the native binding exposes — CRUD, reads, time series, metadata, query, CSV, and the
861
871
  binary/expression subsystems — is documented above and callable.`;
package/src/read.ts CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  decodeFloat64Array,
9
9
  decodeInt64Array,
10
10
  decodePtrArray,
11
- decodeStringArray,
12
11
  decodeStringFromBuf,
13
12
  decodeUint64Array,
14
13
  readPtrOut,
@@ -251,10 +250,11 @@ function readBulkIntegers(
251
250
  fn: string,
252
251
  collection: string,
253
252
  attribute: string,
254
- ): number[][] {
253
+ ): (number | null)[][] {
255
254
  const collBuf = toCString(collection);
256
255
  const attrBuf = toCString(attribute);
257
256
  const outVectors = allocPtrOut();
257
+ const outMasks = allocPtrOut();
258
258
  const outSizes = allocPtrOut();
259
259
  const outCount = allocUint64Out();
260
260
  check(
@@ -263,6 +263,7 @@ function readBulkIntegers(
263
263
  collBuf.buf,
264
264
  attrBuf.buf,
265
265
  outVectors.buf,
266
+ outMasks.buf,
266
267
  outSizes.buf,
267
268
  outCount.buf,
268
269
  ),
@@ -270,14 +271,23 @@ function readBulkIntegers(
270
271
  const count = readUint64Out(outCount);
271
272
  if (count === 0) return [];
272
273
  const vectorsPtr = readPtrOut(outVectors);
274
+ const masksPtr = readPtrOut(outMasks);
273
275
  const sizesPtr = readPtrOut(outSizes);
274
276
  const vectorPtrs = decodePtrArray(vectorsPtr, count);
277
+ const maskPtrs = decodePtrArray(masksPtr, count);
275
278
  const sizes = decodeUint64Array(sizesPtr, count);
276
- const result: number[][] = new Array(count);
279
+ const result: (number | null)[][] = new Array(count);
277
280
  for (let i = 0; i < count; i++) {
278
- result[i] = sizes[i] === 0 ? [] : decodeInt64Array(vectorPtrs[i], sizes[i]);
281
+ if (sizes[i] === 0) {
282
+ result[i] = [];
283
+ continue;
284
+ }
285
+ const values = decodeInt64Array(vectorPtrs[i], sizes[i]);
286
+ const mask = new Uint8Array(toArrayBuffer(maskPtrs[i] as Pointer, 0, sizes[i]));
287
+ result[i] = values.map((v, j) => (mask[j] ? v : null));
279
288
  }
280
289
  lib.quiver_database_free_integer_vectors(vectorsPtr, sizesPtr, BigInt(count));
290
+ lib.quiver_database_free_masks(masksPtr, BigInt(count));
281
291
  return result;
282
292
  }
283
293
 
@@ -287,10 +297,11 @@ function readBulkFloats(
287
297
  fn: string,
288
298
  collection: string,
289
299
  attribute: string,
290
- ): number[][] {
300
+ ): (number | null)[][] {
291
301
  const collBuf = toCString(collection);
292
302
  const attrBuf = toCString(attribute);
293
303
  const outVectors = allocPtrOut();
304
+ const outMasks = allocPtrOut();
294
305
  const outSizes = allocPtrOut();
295
306
  const outCount = allocUint64Out();
296
307
  check(
@@ -299,6 +310,7 @@ function readBulkFloats(
299
310
  collBuf.buf,
300
311
  attrBuf.buf,
301
312
  outVectors.buf,
313
+ outMasks.buf,
302
314
  outSizes.buf,
303
315
  outCount.buf,
304
316
  ),
@@ -306,14 +318,23 @@ function readBulkFloats(
306
318
  const count = readUint64Out(outCount);
307
319
  if (count === 0) return [];
308
320
  const vectorsPtr = readPtrOut(outVectors);
321
+ const masksPtr = readPtrOut(outMasks);
309
322
  const sizesPtr = readPtrOut(outSizes);
310
323
  const vectorPtrs = decodePtrArray(vectorsPtr, count);
324
+ const maskPtrs = decodePtrArray(masksPtr, count);
311
325
  const sizes = decodeUint64Array(sizesPtr, count);
312
- const result: number[][] = new Array(count);
326
+ const result: (number | null)[][] = new Array(count);
313
327
  for (let i = 0; i < count; i++) {
314
- result[i] = sizes[i] === 0 ? [] : decodeFloat64Array(vectorPtrs[i], sizes[i]);
328
+ if (sizes[i] === 0) {
329
+ result[i] = [];
330
+ continue;
331
+ }
332
+ const values = decodeFloat64Array(vectorPtrs[i], sizes[i]);
333
+ const mask = new Uint8Array(toArrayBuffer(maskPtrs[i] as Pointer, 0, sizes[i]));
334
+ result[i] = values.map((v, j) => (mask[j] ? v : null));
315
335
  }
316
336
  lib.quiver_database_free_float_vectors(vectorsPtr, sizesPtr, BigInt(count));
337
+ lib.quiver_database_free_masks(masksPtr, BigInt(count));
317
338
  return result;
318
339
  }
319
340
 
@@ -323,7 +344,7 @@ function readBulkStrings(
323
344
  fn: string,
324
345
  collection: string,
325
346
  attribute: string,
326
- ): string[][] {
347
+ ): (string | null)[][] {
327
348
  const collBuf = toCString(collection);
328
349
  const attrBuf = toCString(attribute);
329
350
  const outVectors = allocPtrOut();
@@ -345,9 +366,16 @@ function readBulkStrings(
345
366
  const sizesPtr = readPtrOut(outSizes);
346
367
  const vectorPtrs = decodePtrArray(vectorsPtr, count);
347
368
  const sizes = decodeUint64Array(sizesPtr, count);
348
- const result: string[][] = new Array(count);
369
+ const result: (string | null)[][] = new Array(count);
349
370
  for (let i = 0; i < count; i++) {
350
- result[i] = sizes[i] === 0 ? [] : decodeStringArray(vectorPtrs[i], sizes[i]);
371
+ // A NULL cell is a NULL char* entry, so decode the pointer array and map NULL -> null
372
+ // (decodeStringArray would construct "" from a NULL pointer instead).
373
+ result[i] =
374
+ sizes[i] === 0
375
+ ? []
376
+ : decodePtrArray(vectorPtrs[i], sizes[i]).map((cell) =>
377
+ cell === null ? null : new CString(cell).toString(),
378
+ );
351
379
  }
352
380
  lib.quiver_database_free_string_vectors(vectorsPtr, sizesPtr, BigInt(count));
353
381
  return result;
@@ -357,7 +385,7 @@ Database.prototype.readVectorIntegers = function (
357
385
  this: Database,
358
386
  collection: string,
359
387
  attribute: string,
360
- ): number[][] {
388
+ ): (number | null)[][] {
361
389
  return readBulkIntegers(
362
390
  getSymbols(),
363
391
  this._handle,
@@ -366,15 +394,12 @@ Database.prototype.readVectorIntegers = function (
366
394
  attribute,
367
395
  );
368
396
  };
369
- /**
370
- * NULL cells are dropped and only elements that own rows are returned, so the result is not
371
- * positionally aligned with `readElementIds` (unlike `readScalarBooleans`).
372
- */
397
+ /** One entry per element; within an entry a SQL NULL cell is `null`. */
373
398
  Database.prototype.readVectorBooleans = function (
374
399
  this: Database,
375
400
  collection: string,
376
401
  attribute: string,
377
- ): boolean[][] {
402
+ ): (boolean | null)[][] {
378
403
  return this.readVectorIntegers(collection, attribute).map((values) =>
379
404
  values.map((value) => integerToBoolean(value, collection, attribute)),
380
405
  );
@@ -383,7 +408,7 @@ Database.prototype.readVectorFloats = function (
383
408
  this: Database,
384
409
  collection: string,
385
410
  attribute: string,
386
- ): number[][] {
411
+ ): (number | null)[][] {
387
412
  return readBulkFloats(
388
413
  getSymbols(),
389
414
  this._handle,
@@ -396,7 +421,7 @@ Database.prototype.readVectorStrings = function (
396
421
  this: Database,
397
422
  collection: string,
398
423
  attribute: string,
399
- ): string[][] {
424
+ ): (string | null)[][] {
400
425
  return readBulkStrings(
401
426
  getSymbols(),
402
427
  this._handle,
@@ -409,7 +434,7 @@ Database.prototype.readSetIntegers = function (
409
434
  this: Database,
410
435
  collection: string,
411
436
  attribute: string,
412
- ): number[][] {
437
+ ): (number | null)[][] {
413
438
  return readBulkIntegers(
414
439
  getSymbols(),
415
440
  this._handle,
@@ -418,12 +443,12 @@ Database.prototype.readSetIntegers = function (
418
443
  attribute,
419
444
  );
420
445
  };
421
- /** Same alignment caveat as `readVectorBooleans`: NULL cells dropped, only ids that own rows. */
446
+ /** Same contract as `readVectorBooleans`: one entry per element, and a NULL cell is `null`. */
422
447
  Database.prototype.readSetBooleans = function (
423
448
  this: Database,
424
449
  collection: string,
425
450
  attribute: string,
426
- ): boolean[][] {
451
+ ): (boolean | null)[][] {
427
452
  return this.readSetIntegers(collection, attribute).map((values) =>
428
453
  values.map((value) => integerToBoolean(value, collection, attribute)),
429
454
  );
@@ -432,7 +457,7 @@ Database.prototype.readSetFloats = function (
432
457
  this: Database,
433
458
  collection: string,
434
459
  attribute: string,
435
- ): number[][] {
460
+ ): (number | null)[][] {
436
461
  return readBulkFloats(
437
462
  getSymbols(),
438
463
  this._handle,
@@ -445,7 +470,7 @@ Database.prototype.readSetStrings = function (
445
470
  this: Database,
446
471
  collection: string,
447
472
  attribute: string,
448
- ): string[][] {
473
+ ): (string | null)[][] {
449
474
  return readBulkStrings(
450
475
  getSymbols(),
451
476
  this._handle,
@@ -464,10 +489,11 @@ function readByIdIntegers(
464
489
  collection: string,
465
490
  attribute: string,
466
491
  id: number,
467
- ): number[] {
492
+ ): (number | null)[] {
468
493
  const collBuf = toCString(collection);
469
494
  const attrBuf = toCString(attribute);
470
495
  const outValues = allocPtrOut();
496
+ const outMask = allocPtrOut();
471
497
  const outCount = allocUint64Out();
472
498
  check(
473
499
  (lib as Record<string, Function>)[fn](
@@ -476,14 +502,19 @@ function readByIdIntegers(
476
502
  attrBuf.buf,
477
503
  BigInt(id),
478
504
  outValues.buf,
505
+ outMask.buf,
479
506
  outCount.buf,
480
507
  ),
481
508
  );
482
509
  const count = readUint64Out(outCount);
483
510
  if (count === 0) return [];
484
511
  const arrPtr = readPtrOut(outValues);
485
- const result = decodeInt64Array(arrPtr, count);
512
+ const maskPtr = readPtrOut(outMask);
513
+ const values = decodeInt64Array(arrPtr, count);
514
+ const mask = new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, count));
515
+ const result = values.map((v, i) => (mask[i] ? v : null));
486
516
  lib.quiver_database_free_integer_array(arrPtr);
517
+ lib.quiver_database_free_mask(maskPtr);
487
518
  return result;
488
519
  }
489
520
 
@@ -494,10 +525,11 @@ function readByIdFloats(
494
525
  collection: string,
495
526
  attribute: string,
496
527
  id: number,
497
- ): number[] {
528
+ ): (number | null)[] {
498
529
  const collBuf = toCString(collection);
499
530
  const attrBuf = toCString(attribute);
500
531
  const outValues = allocPtrOut();
532
+ const outMask = allocPtrOut();
501
533
  const outCount = allocUint64Out();
502
534
  check(
503
535
  (lib as Record<string, Function>)[fn](
@@ -506,14 +538,19 @@ function readByIdFloats(
506
538
  attrBuf.buf,
507
539
  BigInt(id),
508
540
  outValues.buf,
541
+ outMask.buf,
509
542
  outCount.buf,
510
543
  ),
511
544
  );
512
545
  const count = readUint64Out(outCount);
513
546
  if (count === 0) return [];
514
547
  const arrPtr = readPtrOut(outValues);
515
- const result = decodeFloat64Array(arrPtr, count);
548
+ const maskPtr = readPtrOut(outMask);
549
+ const values = decodeFloat64Array(arrPtr, count);
550
+ const mask = new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, count));
551
+ const result = values.map((v, i) => (mask[i] ? v : null));
516
552
  lib.quiver_database_free_float_array(arrPtr);
553
+ lib.quiver_database_free_mask(maskPtr);
517
554
  return result;
518
555
  }
519
556
 
@@ -524,7 +561,7 @@ function readByIdStrings(
524
561
  collection: string,
525
562
  attribute: string,
526
563
  id: number,
527
- ): string[] {
564
+ ): (string | null)[] {
528
565
  const collBuf = toCString(collection);
529
566
  const attrBuf = toCString(attribute);
530
567
  const outValues = allocPtrOut();
@@ -542,7 +579,11 @@ function readByIdStrings(
542
579
  const count = readUint64Out(outCount);
543
580
  if (count === 0) return [];
544
581
  const arrPtr = readPtrOut(outValues);
545
- const result = decodeStringArray(arrPtr, count);
582
+ // A NULL cell is a NULL char* entry, so decode the pointer array and map NULL -> null
583
+ // (decodeStringArray would construct "" from a NULL pointer instead).
584
+ const result = decodePtrArray(arrPtr, count).map((cell) =>
585
+ cell === null ? null : new CString(cell).toString(),
586
+ );
546
587
  lib.quiver_database_free_string_array(arrPtr, BigInt(count));
547
588
  return result;
548
589
  }
@@ -552,7 +593,7 @@ Database.prototype.readVectorIntegersById = function (
552
593
  collection: string,
553
594
  attribute: string,
554
595
  id: number,
555
- ): number[] {
596
+ ): (number | null)[] {
556
597
  return readByIdIntegers(
557
598
  getSymbols(),
558
599
  this._handle,
@@ -567,7 +608,7 @@ Database.prototype.readVectorBooleansById = function (
567
608
  collection: string,
568
609
  attribute: string,
569
610
  id: number,
570
- ): boolean[] {
611
+ ): (boolean | null)[] {
571
612
  return this.readVectorIntegersById(collection, attribute, id).map((value) =>
572
613
  integerToBoolean(value, collection, attribute),
573
614
  );
@@ -577,7 +618,7 @@ Database.prototype.readVectorFloatsById = function (
577
618
  collection: string,
578
619
  attribute: string,
579
620
  id: number,
580
- ): number[] {
621
+ ): (number | null)[] {
581
622
  return readByIdFloats(
582
623
  getSymbols(),
583
624
  this._handle,
@@ -592,7 +633,7 @@ Database.prototype.readVectorStringsById = function (
592
633
  collection: string,
593
634
  attribute: string,
594
635
  id: number,
595
- ): string[] {
636
+ ): (string | null)[] {
596
637
  return readByIdStrings(
597
638
  getSymbols(),
598
639
  this._handle,
@@ -607,7 +648,7 @@ Database.prototype.readSetIntegersById = function (
607
648
  collection: string,
608
649
  attribute: string,
609
650
  id: number,
610
- ): number[] {
651
+ ): (number | null)[] {
611
652
  return readByIdIntegers(
612
653
  getSymbols(),
613
654
  this._handle,
@@ -622,7 +663,7 @@ Database.prototype.readSetBooleansById = function (
622
663
  collection: string,
623
664
  attribute: string,
624
665
  id: number,
625
- ): boolean[] {
666
+ ): (boolean | null)[] {
626
667
  return this.readSetIntegersById(collection, attribute, id).map((value) =>
627
668
  integerToBoolean(value, collection, attribute),
628
669
  );
@@ -632,7 +673,7 @@ Database.prototype.readSetFloatsById = function (
632
673
  collection: string,
633
674
  attribute: string,
634
675
  id: number,
635
- ): number[] {
676
+ ): (number | null)[] {
636
677
  return readByIdFloats(
637
678
  getSymbols(),
638
679
  this._handle,
@@ -647,7 +688,7 @@ Database.prototype.readSetStringsById = function (
647
688
  collection: string,
648
689
  attribute: string,
649
690
  id: number,
650
- ): string[] {
691
+ ): (string | null)[] {
651
692
  return readByIdStrings(
652
693
  getSymbols(),
653
694
  this._handle,