quiverdb 0.10.3 → 0.10.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/mod.ts CHANGED
@@ -15,6 +15,7 @@ export type {
15
15
  ArrayValue,
16
16
  CsvOptions,
17
17
  ElementData,
18
+ GroupColumns,
18
19
  GroupMetadata,
19
20
  QueryParam,
20
21
  ScalarMetadata,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/database.ts CHANGED
@@ -192,13 +192,13 @@ export class Database {
192
192
  collection: string,
193
193
  group: string,
194
194
  id: number,
195
- data: TimeSeriesData,
195
+ data: GroupColumns,
196
196
  ) => void;
197
197
  declare updateTimeSeriesGroupByLabel: (
198
198
  collection: string,
199
199
  group: string,
200
200
  label: string,
201
- data: TimeSeriesData,
201
+ data: GroupColumns,
202
202
  ) => void;
203
203
  declare updateVectorGroup: (
204
204
  collection: string,
@@ -228,13 +228,13 @@ export class Database {
228
228
  collection: string,
229
229
  group: string,
230
230
  id: number,
231
- row: Record<string, number | bigint | string>,
231
+ row: Record<string, number | bigint | string | boolean>,
232
232
  ) => void;
233
233
  declare upsertTimeSeriesRowByLabel: (
234
234
  collection: string,
235
235
  group: string,
236
236
  label: string,
237
- row: Record<string, number | bigint | string>,
237
+ row: Record<string, number | bigint | string | boolean>,
238
238
  ) => void;
239
239
  declare hasTimeSeriesFiles: (collection: string) => boolean;
240
240
  declare listTimeSeriesFilesColumns: (collection: string) => string[];
@@ -11,7 +11,7 @@ import type { NativePointer } from "./loader.ts";
11
11
  import { DATA_TYPE_FLOAT, DATA_TYPE_INTEGER, DATA_TYPE_STRING, type Allocation } from "./types.ts";
12
12
 
13
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)[]>;
14
+ export type GroupColumns = Record<string, (number | string | boolean | null)[]>;
15
15
 
16
16
  /**
17
17
  * The parallel-array signature every columnar group update C function shares
@@ -94,7 +94,16 @@ export function updateGroupColumns(
94
94
  const maskPtrs: (Pointer | null)[] = [];
95
95
 
96
96
  for (let c = 0; c < columnCount; c++) {
97
- const [colName, values] = entries[c];
97
+ const [colName, rawValues] = entries[c];
98
+ // SQLite has no boolean type: a boolean is INTEGER 1/0, as in setElementField and
99
+ // marshalParams. Normalizing per cell before the dispatch (rather than adding a boolean
100
+ // branch after it) is what makes a mixed [true, 5] column write 1 and 5 instead of
101
+ // truthiness-mapping every cell, and matches Python's per-cell `int(v)`. A string column is
102
+ // left alone so normalizing cannot change what a mixed ['a', true] column already wrote.
103
+ const isStringColumn = typeof rawValues.find((v) => v !== null) === "string";
104
+ const values = isStringColumn
105
+ ? rawValues
106
+ : rawValues.map((v) => (typeof v === "boolean" ? (v ? 1 : 0) : v));
98
107
  const first = values.find((v) => v !== null);
99
108
 
100
109
  // Mask via direct indexing — never a DataView, to avoid the documented
package/src/lua-api.ts CHANGED
@@ -51,13 +51,25 @@ Lua values map to Quiver column values as follows:
51
51
  | integer | INTEGER | Also accepted for REAL columns (coerced to real). |
52
52
  | number (float) | REAL | A float is rejected for an INTEGER column. |
53
53
  | string | TEXT | Also used for \`date_time\` columns (ISO 8601). |
54
+ | boolean | INTEGER 1/0 | SQLite has no boolean type; \`true\` writes 1, \`false\` 0. |
54
55
  | \`nil\` | NULL | In query params, file paths, ts rows, relations.|
55
56
  | table (1-indexed) | array | Used for vectors/sets and column-oriented data.|
56
57
 
57
- **Unsupported types throw.** Passing a boolean, a function, or a nested table where a scalar is
58
- expected raises an error rather than silently dropping the value. This applies to element
59
- attributes, time-series rows, and query parameters. A skipped positional query parameter would
60
- shift every later parameter and bind NULL to the trailing placeholder, so this is rejected loudly.
58
+ A boolean is accepted **wherever an integer is** — element scalars and arrays, query parameters,
59
+ the group writers, and \`upsert_time_series_row\` — so it also reaches a REAL column through the
60
+ usual int-for-REAL coercion. Reading is the asymmetric half: there are no boolean readers in Lua
61
+ (unlike Julia/Dart/Python/JS), so a stored flag comes back as \`0\`/\`1\` from
62
+ \`read_scalar_integers\`. Compare against 0 rather than truth-testing —
63
+ \`read_scalar_integers(c, a)[i] == 1\` — because in Lua a \`nil\` from a NULL cell is *not* equal to
64
+ 0 and \`nil ~= 0\` evaluates to \`true\`.
65
+
66
+ The one place a boolean is deliberately refused is \`db:update_relation\`, where the argument is a
67
+ target label and only \`nil\` (or omitting it) may clear the relation.
68
+
69
+ **Unsupported types throw.** Passing a function or a nested table where a scalar is expected raises
70
+ an error rather than silently dropping the value. This applies to element attributes, time-series
71
+ rows, and query parameters. A skipped positional query parameter would shift every later parameter
72
+ and bind NULL to the trailing placeholder, so this is rejected loudly.
61
73
 
62
74
  Dates are plain strings in ISO 8601 format: \`YYYY-MM-DDTHH:MM:SS\`. (Lua keeps a string-based
63
75
  datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Python.) The time part
@@ -368,6 +380,13 @@ db:read_element_by_id(collection, id) -- scalars + vectors + sets mer
368
380
  \`read_element_by_id\` merges every scalar, vector, and set for the element into a single table.
369
381
  Scalar attributes with no value come back as \`nil\`.
370
382
 
383
+ **Group columns are returned densely, with NULL cells dropped.** \`read_vectors_by_id\` and
384
+ \`read_sets_by_id\` read each column independently, and a column read skips its NULL cells — so two
385
+ columns of the same group are **not** positionally aligned with each other whenever one is
386
+ nullable (e.g. an \`ON DELETE SET NULL\` relation). Do not zip them into rows. There is no
387
+ row-aligned group read in Lua; if you need per-row alignment across a nullable group, do that read
388
+ in the host binding instead.
389
+
371
390
  ---
372
391
 
373
392
  ## Time series
@@ -681,7 +700,9 @@ and \`unit\` default to \`""\`; \`labels\`, \`dimensions\`, \`dimension_sizes\`,
681
700
 
682
701
  ## What Lua does *not* expose
683
702
 
684
- DateTime wrapper helpers (Lua uses ISO 8601 strings) and \`_by_id\` single-scalar variants (use the
685
- composite by-id readers or the bulk readers instead). Everything else the native binding exposes —
686
- CRUD, reads, time series, metadata, query, CSV, and the binary/expression subsystems — is
687
- documented above and callable.`;
703
+ DateTime wrapper helpers (Lua uses ISO 8601 strings), boolean *reader* helpers (a stored flag reads
704
+ back as \`0\`/\`1\` — writing a boolean is supported), \`_by_id\` single-scalar variants (use the
705
+ composite by-id readers or the bulk readers instead), and the row-aligned whole-group readers the
706
+ other bindings have (hence the null-dropping caveat under composite by-id reads). Everything else
707
+ the native binding exposes — CRUD, reads, time series, metadata, query, CSV, and the
708
+ binary/expression subsystems — is documented above and callable.`;
@@ -17,7 +17,7 @@ import {
17
17
  readUint64Out,
18
18
  toCString,
19
19
  } from "./ffi-helpers.ts";
20
- import { updateGroupColumns } from "./group-columns.ts";
20
+ import { type GroupColumns, updateGroupColumns } from "./group-columns.ts";
21
21
  import { getSymbols, type NativePointer } from "./loader.ts";
22
22
  import {
23
23
  type Allocation,
@@ -187,7 +187,7 @@ Database.prototype.updateTimeSeriesGroup = function (
187
187
  collection: string,
188
188
  group: string,
189
189
  id: number,
190
- data: TimeSeriesData,
190
+ data: GroupColumns,
191
191
  ): void {
192
192
  updateGroupColumns(
193
193
  this._handle,
@@ -206,7 +206,7 @@ Database.prototype.updateTimeSeriesGroupByLabel = function (
206
206
  collection: string,
207
207
  group: string,
208
208
  label: string,
209
- data: TimeSeriesData,
209
+ data: GroupColumns,
210
210
  ): void {
211
211
  updateGroupColumns(
212
212
  this._handle,
@@ -242,11 +242,12 @@ type UpsertRowFn = (
242
242
  */
243
243
  function upsertRowColumns(
244
244
  handle: NativePointer,
245
+ caller: string,
245
246
  upsert: UpsertRowFn,
246
247
  collection: string,
247
248
  group: string,
248
249
  key: number | string,
249
- row: Record<string, number | bigint | string>,
250
+ row: Record<string, number | bigint | string | boolean>,
250
251
  ): void {
251
252
  const collBuf = toCString(collection);
252
253
  const grpBuf = toCString(group);
@@ -264,7 +265,11 @@ function upsertRowColumns(
264
265
  const dataPtrs: (Pointer | null)[] = [];
265
266
 
266
267
  for (let c = 0; c < columnCount; c++) {
267
- const value = entries[c][1];
268
+ const [colName, raw] = entries[c];
269
+ // SQLite has no boolean type: a boolean is INTEGER 1/0. Normalized here rather than tested in
270
+ // the INTEGER condition below, because Number.isInteger(true) is false — a boolean otherwise
271
+ // reaches allocNativeFloat64 and lands in the column as FLOAT 1.0, silently the wrong type.
272
+ const value = typeof raw === "boolean" ? (raw ? 1 : 0) : raw;
268
273
  if (typeof value === "string") {
269
274
  typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
270
275
  const { table, keepalive: strPtrs } = allocNativeStringArray([value]);
@@ -275,11 +280,17 @@ function upsertRowColumns(
275
280
  const p = allocNativeInt64([value]);
276
281
  keepalive.push(p);
277
282
  dataPtrs.push(p.ptr);
278
- } else {
283
+ } else if (typeof value === "number") {
279
284
  typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
280
- const p = allocNativeFloat64([value as number]);
285
+ const p = allocNativeFloat64([value]);
281
286
  keepalive.push(p);
282
287
  dataPtrs.push(p.ptr);
288
+ } else {
289
+ // Mirrors updateGroupColumns: never let null/undefined/an object fall through to the FLOAT
290
+ // branch, where Number(null) is 0 and anything else is NaN — both written with no error.
291
+ throw new QuiverError(
292
+ `Cannot ${caller}: column '${colName}' has unsupported value type ${typeof value}`,
293
+ );
283
294
  }
284
295
  }
285
296
 
@@ -307,10 +318,11 @@ Database.prototype.upsertTimeSeriesRow = function (
307
318
  collection: string,
308
319
  group: string,
309
320
  id: number,
310
- row: Record<string, number | bigint | string>,
321
+ row: Record<string, number | bigint | string | boolean>,
311
322
  ): void {
312
323
  upsertRowColumns(
313
324
  this._handle,
325
+ "upsertTimeSeriesRow",
314
326
  getSymbols().quiver_database_upsert_time_series_row,
315
327
  collection,
316
328
  group,
@@ -325,10 +337,11 @@ Database.prototype.upsertTimeSeriesRowByLabel = function (
325
337
  collection: string,
326
338
  group: string,
327
339
  label: string,
328
- row: Record<string, number | bigint | string>,
340
+ row: Record<string, number | bigint | string | boolean>,
329
341
  ): void {
330
342
  upsertRowColumns(
331
343
  this._handle,
344
+ "upsertTimeSeriesRowByLabel",
332
345
  getSymbols().quiver_database_upsert_time_series_row_by_label,
333
346
  collection,
334
347
  group,