quiverdb 0.12.5 → 0.12.6

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
@@ -135,8 +135,8 @@ bun run example.ts
135
135
  - `queryBoolean(sql, parameters?)` -- Query returning an INTEGER-backed boolean or null
136
136
  - `queryFloat(sql, parameters?)` -- Query returning float or null
137
137
 
138
- Parameters are passed as an array of `number | boolean | string | null` (a `boolean` binds as the
139
- INTEGER 1 or 0).
138
+ Parameters are passed as an array of `number | bigint | boolean | string | null` (a `bigint` binds
139
+ as an exact INTEGER, a `boolean` as the INTEGER 1 or 0).
140
140
 
141
141
  ### Transaction
142
142
 
@@ -182,12 +182,15 @@ Exported types available for TypeScript consumers:
182
182
  - `ArrayValue` -- `number[] | bigint[] | string[]`
183
183
  - `Value` -- `ScalarValue | ArrayValue`
184
184
  - `ElementData` -- `Record<string, Value | undefined>`
185
- - `QueryParam` -- `number | string | null`
185
+ - `QueryParam` -- `number | bigint | boolean | string | null`
186
186
  - `QuiverError` -- Error class for all Quiver operations
187
187
  - `ScalarMetadata` -- Scalar attribute metadata
188
188
  - `GroupMetadata` -- Vector/set/time series group metadata
189
189
  - `TimeSeriesData` -- Time series column data
190
190
  - `CsvOptions` -- CSV import/export options
191
+ - `DatabaseOptions` -- `{ readOnly?: boolean; consoleLevel?: number }` (a `LOG_LEVEL_*` constant)
192
+ - `LOG_LEVEL_DEBUG | INFO | WARN | ERROR | OFF`, `DATA_TYPE_INTEGER | FLOAT | STRING | DATE_TIME | NULL`
193
+ -- constants for `DatabaseOptions.consoleLevel` and metadata `dataType`
191
194
 
192
195
  ## Development
193
196
 
Binary file
Binary file
Binary file
package/mod.ts CHANGED
@@ -11,16 +11,4 @@
11
11
  * ```
12
12
  */
13
13
 
14
- export type {
15
- ArrayValue,
16
- CsvOptions,
17
- ElementData,
18
- GroupColumns,
19
- GroupMetadata,
20
- QueryParam,
21
- ScalarMetadata,
22
- ScalarValue,
23
- TimeSeriesData,
24
- Value,
25
- } from "./src/index.ts";
26
- export { Database, LUA_DB_API_REFERENCE, LuaRunner, QuiverError } from "./src/index.ts";
14
+ export * from "./src/index.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.12.5",
3
+ "version": "0.12.6",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/create.ts CHANGED
@@ -8,7 +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
+ import { type GroupColumns, numericCells, updateGroupColumns } from "./group-columns.ts";
12
12
  import { getSymbols, type NativePointer } from "./loader.ts";
13
13
  import type { ElementData, Value } from "./types.ts";
14
14
 
@@ -17,6 +17,7 @@ type Symbols = ReturnType<typeof getSymbols>;
17
17
  function setElementArray(
18
18
  lib: Symbols,
19
19
  elemPtr: NativePointer,
20
+ caller: string,
20
21
  name: string,
21
22
  values: unknown[],
22
23
  ): void {
@@ -39,27 +40,18 @@ function setElementArray(
39
40
  );
40
41
  }
41
42
 
42
- if (typeof first === "bigint") {
43
- const arr = allocNativeInt64(values as bigint[]);
44
- check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
45
- return;
46
- }
47
-
48
- if (typeof first === "boolean") {
49
- const arr = allocNativeInt64((values as boolean[]).map((v) => (v ? 1 : 0)));
50
- check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
51
- return;
52
- }
53
-
54
- if (typeof first === "number") {
55
- const allIntegers = (values as number[]).every((v) => Number.isInteger(v));
56
- if (allIntegers) {
57
- const arr = allocNativeInt64(values as number[]);
43
+ if (typeof first === "number" || typeof first === "boolean" || typeof first === "bigint") {
44
+ // The null check above leaves no null cell, so the cast only narrows.
45
+ const cells = numericCells(caller, name, values) as (number | bigint)[];
46
+ if (cells.every((v) => typeof v === "bigint" || Number.isInteger(v))) {
47
+ const arr = allocNativeInt64(cells);
58
48
  check(
59
49
  lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null),
60
50
  );
61
51
  } else {
62
- const arr = allocNativeFloat64(values as number[]);
52
+ // A fractional cell makes the array FLOAT; a bigint cell then follows the int-for-REAL rule
53
+ // through Number(), exact up to 2^53 like any JS number.
54
+ const arr = allocNativeFloat64(cells.map(Number));
63
55
  check(lib.quiver_element_set_array_float(elemPtr, nameBuf.buf, arr.buf, values.length, null));
64
56
  }
65
57
  return;
@@ -76,7 +68,13 @@ function setElementArray(
76
68
  throw new QuiverError(`Unsupported array element type for '${name}': ${typeof first}`);
77
69
  }
78
70
 
79
- function setElementField(lib: Symbols, elemPtr: NativePointer, name: string, value: Value): void {
71
+ function setElementField(
72
+ lib: Symbols,
73
+ elemPtr: NativePointer,
74
+ caller: string,
75
+ name: string,
76
+ value: Value,
77
+ ): void {
80
78
  const nameBuf = toCString(name);
81
79
 
82
80
  if (value === null) {
@@ -110,7 +108,7 @@ function setElementField(lib: Symbols, elemPtr: NativePointer, name: string, val
110
108
  }
111
109
 
112
110
  if (Array.isArray(value)) {
113
- setElementArray(lib, elemPtr, name, value);
111
+ setElementArray(lib, elemPtr, caller, name, value);
114
112
  return;
115
113
  }
116
114
 
@@ -132,7 +130,7 @@ Database.prototype.createElement = function (
132
130
  try {
133
131
  for (const [key, value] of Object.entries(data)) {
134
132
  if (value === undefined) continue;
135
- setElementField(lib, elemPtr, key, value);
133
+ setElementField(lib, elemPtr, "createElement", key, value);
136
134
  }
137
135
 
138
136
  const outIdBuf = new Uint8Array(8);
@@ -160,7 +158,7 @@ Database.prototype.updateElement = function (
160
158
  try {
161
159
  for (const [key, value] of Object.entries(data)) {
162
160
  if (value === undefined) continue;
163
- setElementField(lib, elemPtr, key, value);
161
+ setElementField(lib, elemPtr, "updateElement", key, value);
164
162
  }
165
163
  const collBuf = toCString(collection);
166
164
  check(lib.quiver_database_update_element(handle, collBuf.buf, BigInt(id), elemPtr));
@@ -186,7 +184,7 @@ Database.prototype.updateElementByLabel = function (
186
184
  try {
187
185
  for (const [key, value] of Object.entries(data)) {
188
186
  if (value === undefined) continue;
189
- setElementField(lib, elemPtr, key, value);
187
+ setElementField(lib, elemPtr, "updateElementByLabel", key, value);
190
188
  }
191
189
  const collBuf = toCString(collection);
192
190
  const labelBuf = toCString(label);
@@ -24,12 +24,16 @@ import {
24
24
  DATA_TYPE_STRING,
25
25
  } from "./types.ts";
26
26
 
27
- /** Column-oriented group payload: one array of cells per column name, `null` for SQL NULL. */
28
- export type GroupColumns = Record<string, (number | string | boolean | null)[]>;
27
+ /**
28
+ * Column-oriented group payload: one array of cells per column name, `null` for SQL NULL. A
29
+ * `bigint` cell is written as an exact int64 (like createElement); a `boolean` as INTEGER 1/0.
30
+ */
31
+ export type GroupColumns = Record<string, (number | bigint | string | boolean | null)[]>;
29
32
 
30
33
  /**
31
34
  * Column-oriented group read result: one array of cells per column name, `null` for SQL NULL.
32
- * The read-side twin of GroupColumns; no reader produces a boolean, so it admits none.
35
+ * The read-side twin of GroupColumns; no reader produces a boolean or a bigint, so it admits
36
+ * neither.
33
37
  */
34
38
  export type TimeSeriesData = Record<string, (number | string | null)[]>;
35
39
 
@@ -51,6 +55,34 @@ type ColumnUpdateFn = (
51
55
  rowCount: bigint,
52
56
  ) => number;
53
57
 
58
+ /**
59
+ * Check and normalize the cells of a numeric column: one whose first non-null cell is a number, a
60
+ * bigint or a boolean. Shared by updateGroupColumns and setElementArray (create.ts) so the group
61
+ * writers and the element arrays agree on every cell. String columns are never passed here.
62
+ *
63
+ * - A boolean becomes INTEGER 1/0, since SQLite has no boolean type. Per cell, because mapping the
64
+ * whole column from a leading boolean truthiness-mapped the rest and rewrote [true, 5] as
65
+ * [1, 1]. Before the INTEGER/FLOAT choice, because Number.isInteger(true) is false.
66
+ * - A bigint stays a bigint, so an INTEGER column keeps it exact; the caller maps it through
67
+ * Number() only if the column turns out FLOAT.
68
+ * - Any other non-null cell throws, naming the column and the cell. It would fail
69
+ * Number.isInteger, tag the column FLOAT and reach DataView.setFloat64, which converts it with no
70
+ * error: "abc" to NaN, which SQLite stores as NULL, and "2" to 2.
71
+ */
72
+ export function numericCells(
73
+ caller: string,
74
+ name: string,
75
+ values: readonly unknown[],
76
+ ): (number | bigint | null)[] {
77
+ return values.map((v, r) => {
78
+ if (v === null || typeof v === "number" || typeof v === "bigint") return v;
79
+ if (typeof v === "boolean") return v ? 1 : 0;
80
+ throw new QuiverError(
81
+ `Cannot ${caller}: numeric column '${name}' has unsupported value type ${typeof v} in cell ${r}`,
82
+ );
83
+ });
84
+ }
85
+
54
86
  /**
55
87
  * Marshal a column-oriented payload and forward it to one of the columnar group update C
56
88
  * functions. Shared by updateTimeSeriesGroup / updateVectorGroup / updateSetGroup and their
@@ -114,16 +146,7 @@ export function updateGroupColumns(
114
146
  const maskPtrs: (Pointer | null)[] = [];
115
147
 
116
148
  for (let c = 0; c < columnCount; c++) {
117
- const [colName, rawValues] = entries[c];
118
- // SQLite has no boolean type: a boolean is INTEGER 1/0, as in setElementField and
119
- // marshalParams. Normalizing per cell before the dispatch (rather than adding a boolean
120
- // branch after it) is what makes a mixed [true, 5] column write 1 and 5 instead of
121
- // truthiness-mapping every cell, and matches Python's per-cell `int(v)`. A string column is
122
- // left alone so normalizing cannot change what a mixed ['a', true] column already wrote.
123
- const isStringColumn = typeof rawValues.find((v) => v !== null) === "string";
124
- const values = isStringColumn
125
- ? rawValues
126
- : rawValues.map((v) => (typeof v === "boolean" ? (v ? 1 : 0) : v));
149
+ const [colName, values] = entries[c];
127
150
  const first = values.find((v) => v !== null);
128
151
 
129
152
  // Mask via direct indexing — never a DataView, to avoid the documented
@@ -141,23 +164,29 @@ export function updateGroupColumns(
141
164
  keepalive.push(p);
142
165
  dataPtrs.push(p.ptr);
143
166
  } else if (typeof first === "string") {
167
+ // Not checked by numericCells: a mixed ['a', true] column still writes what it always wrote.
144
168
  typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
145
169
  const { table, keepalive: strPtrs } = allocNativeStringArray(
146
170
  values.map((v) => (v === null ? null : (v as string))),
147
171
  );
148
172
  keepalive.push(table, ...strPtrs);
149
173
  dataPtrs.push(table.ptr);
150
- } else if (typeof first === "number") {
151
- const nonNull = values.filter((v) => v !== null) as number[];
152
- const sanitized = values.map((v) => (v === null ? 0 : (v as number)));
153
- if (nonNull.every((v) => Number.isInteger(v))) {
174
+ } else if (
175
+ typeof first === "number" ||
176
+ typeof first === "boolean" ||
177
+ typeof first === "bigint"
178
+ ) {
179
+ const cells = numericCells(caller, colName, values);
180
+ if (cells.every((v) => v === null || typeof v === "bigint" || Number.isInteger(v))) {
154
181
  typesDv.setInt32(c * 4, DATA_TYPE_INTEGER, true);
155
- const p = allocNativeInt64(sanitized);
182
+ const p = allocNativeInt64(cells.map((v) => v ?? 0));
156
183
  keepalive.push(p);
157
184
  dataPtrs.push(p.ptr);
158
185
  } else {
186
+ // A fractional cell makes the column FLOAT; a bigint cell then follows the int-for-REAL
187
+ // rule through Number(), exact up to 2^53 like any JS number.
159
188
  typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
160
- const p = allocNativeFloat64(sanitized);
189
+ const p = allocNativeFloat64(cells.map((v) => (v === null ? 0 : Number(v))));
161
190
  keepalive.push(p);
162
191
  dataPtrs.push(p.ptr);
163
192
  }
package/src/index.ts CHANGED
@@ -24,6 +24,11 @@ export type {
24
24
  Value,
25
25
  } from "./types.ts";
26
26
  export {
27
+ DATA_TYPE_DATE_TIME,
28
+ DATA_TYPE_FLOAT,
29
+ DATA_TYPE_INTEGER,
30
+ DATA_TYPE_NULL,
31
+ DATA_TYPE_STRING,
27
32
  LOG_LEVEL_DEBUG,
28
33
  LOG_LEVEL_ERROR,
29
34
  LOG_LEVEL_INFO,
package/src/query.ts CHANGED
@@ -50,6 +50,11 @@ function marshalParams(parameters?: QueryParam[]): {
50
50
  const native = allocNativeInt64([p ? 1 : 0]);
51
51
  keepalive.push(native);
52
52
  valuesDv.setBigInt64(i * 8, nativeAddress(native.ptr), true);
53
+ } else if (typeof p === "bigint") {
54
+ typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
55
+ const native = allocNativeInt64([p]);
56
+ keepalive.push(native);
57
+ valuesDv.setBigInt64(i * 8, nativeAddress(native.ptr), true);
53
58
  } else if (typeof p === "number") {
54
59
  if (Number.isInteger(p)) {
55
60
  typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
package/src/types.ts CHANGED
@@ -26,7 +26,8 @@ export type ScalarValue = number | bigint | boolean | string | null;
26
26
  export type ArrayValue = number[] | bigint[] | boolean[] | string[];
27
27
  export type Value = ScalarValue | ArrayValue;
28
28
  export type ElementData = Record<string, Value | undefined>;
29
- export type QueryParam = number | boolean | string | null;
29
+ /** A `bigint` binds as an exact INTEGER; a `boolean` as INTEGER 1/0. */
30
+ export type QueryParam = number | bigint | boolean | string | null;
30
31
 
31
32
  /** Native memory allocation result. Callers MUST hold `buf` in scope to prevent GC. */
32
33
  export type Allocation = {