quiverdb 0.12.3 → 0.12.5
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 +2 -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 +1 -1
- package/src/composites.ts +4 -4
- package/src/create.ts +10 -0
- package/src/database.ts +67 -20
- package/src/group-columns.ts +143 -3
- package/src/index.ts +1 -2
- package/src/loader.ts +21 -15
- package/src/lua-api.ts +28 -15
- package/src/query.ts +47 -56
- package/src/read.ts +134 -37
- package/src/time-series.ts +33 -103
package/README.md
CHANGED
|
@@ -104,6 +104,8 @@ bun run example.ts
|
|
|
104
104
|
- `readSetBooleansById(collection, attribute, id)` -- Read INTEGER-backed boolean set
|
|
105
105
|
- `readSetFloatsById(collection, attribute, id)` -- Read float set
|
|
106
106
|
- `readSetStringsById(collection, attribute, id)` -- Read string set
|
|
107
|
+
- `readVectorGroupById(collection, group, id)` -- Read a whole vector group as rows (`null` for a SQL NULL cell)
|
|
108
|
+
- `readSetGroupById(collection, group, id)` -- Read a whole set group as rows (`null` for a SQL NULL cell)
|
|
107
109
|
|
|
108
110
|
### Read (IDs)
|
|
109
111
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
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,16 @@ function setElementArray(
|
|
|
29
29
|
|
|
30
30
|
const first = values[0];
|
|
31
31
|
|
|
32
|
+
// A vector/set/time-series read returns a NULL cell as null. The element surface is non-null
|
|
33
|
+
// (NULL cells go through the group writers), whatever the cell's position or the array's type:
|
|
34
|
+
// the numeric setters below would store a null as 0 / false, and a string array would store an
|
|
35
|
+
// `undefined` as the text "undefined". `includes`, not `some`: it also sees a sparse array's holes.
|
|
36
|
+
if (values.includes(null) || values.includes(undefined)) {
|
|
37
|
+
throw new QuiverError(
|
|
38
|
+
`Unsupported null cell in array '${name}': write NULL cells with updateVectorGroup, updateSetGroup or updateTimeSeriesGroup`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
if (typeof first === "bigint") {
|
|
33
43
|
const arr = allocNativeInt64(values as bigint[]);
|
|
34
44
|
check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
package/src/database.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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
|
+
import type { GroupColumns, TimeSeriesData } from "./group-columns.ts";
|
|
5
5
|
import type { NativePointer } from "./loader.ts";
|
|
6
6
|
import { getSymbols } from "./loader.ts";
|
|
7
7
|
import type { GroupMetadata, ScalarMetadata } from "./metadata.ts";
|
|
8
|
-
import type { TimeSeriesData } from "./time-series.ts";
|
|
9
8
|
import type { DatabaseOptions, ElementData, QueryParam } from "./types.ts";
|
|
10
9
|
|
|
11
10
|
export class Database {
|
|
@@ -138,22 +137,64 @@ export class Database {
|
|
|
138
137
|
) => string | null;
|
|
139
138
|
declare readElementIds: (collection: string) => number[];
|
|
140
139
|
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: (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
declare
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
declare
|
|
156
|
-
|
|
140
|
+
declare readVectorIntegers: (collection: string, attribute: string) => (number | null)[][];
|
|
141
|
+
declare readVectorBooleans: (collection: string, attribute: string) => (boolean | null)[][];
|
|
142
|
+
declare readVectorFloats: (collection: string, attribute: string) => (number | null)[][];
|
|
143
|
+
declare readVectorStrings: (collection: string, attribute: string) => (string | null)[][];
|
|
144
|
+
declare readVectorIntegersById: (
|
|
145
|
+
collection: string,
|
|
146
|
+
attribute: string,
|
|
147
|
+
id: number,
|
|
148
|
+
) => (number | null)[];
|
|
149
|
+
declare readVectorBooleansById: (
|
|
150
|
+
collection: string,
|
|
151
|
+
attribute: string,
|
|
152
|
+
id: number,
|
|
153
|
+
) => (boolean | null)[];
|
|
154
|
+
declare readVectorFloatsById: (
|
|
155
|
+
collection: string,
|
|
156
|
+
attribute: string,
|
|
157
|
+
id: number,
|
|
158
|
+
) => (number | null)[];
|
|
159
|
+
declare readVectorStringsById: (
|
|
160
|
+
collection: string,
|
|
161
|
+
attribute: string,
|
|
162
|
+
id: number,
|
|
163
|
+
) => (string | null)[];
|
|
164
|
+
declare readSetIntegers: (collection: string, attribute: string) => (number | null)[][];
|
|
165
|
+
declare readSetBooleans: (collection: string, attribute: string) => (boolean | null)[][];
|
|
166
|
+
declare readSetFloats: (collection: string, attribute: string) => (number | null)[][];
|
|
167
|
+
declare readSetStrings: (collection: string, attribute: string) => (string | null)[][];
|
|
168
|
+
declare readSetIntegersById: (
|
|
169
|
+
collection: string,
|
|
170
|
+
attribute: string,
|
|
171
|
+
id: number,
|
|
172
|
+
) => (number | null)[];
|
|
173
|
+
declare readSetBooleansById: (
|
|
174
|
+
collection: string,
|
|
175
|
+
attribute: string,
|
|
176
|
+
id: number,
|
|
177
|
+
) => (boolean | null)[];
|
|
178
|
+
declare readSetFloatsById: (
|
|
179
|
+
collection: string,
|
|
180
|
+
attribute: string,
|
|
181
|
+
id: number,
|
|
182
|
+
) => (number | null)[];
|
|
183
|
+
declare readSetStringsById: (
|
|
184
|
+
collection: string,
|
|
185
|
+
attribute: string,
|
|
186
|
+
id: number,
|
|
187
|
+
) => (string | null)[];
|
|
188
|
+
declare readVectorGroupById: (
|
|
189
|
+
collection: string,
|
|
190
|
+
group: string,
|
|
191
|
+
id: number,
|
|
192
|
+
) => Record<string, number | string | null>[];
|
|
193
|
+
declare readSetGroupById: (
|
|
194
|
+
collection: string,
|
|
195
|
+
group: string,
|
|
196
|
+
id: number,
|
|
197
|
+
) => Record<string, number | string | null>[];
|
|
157
198
|
|
|
158
199
|
// --- Queries (implemented in query.ts) ---
|
|
159
200
|
declare queryString: (sql: string, parameters?: QueryParam[]) => string | null;
|
|
@@ -268,6 +309,12 @@ export class Database {
|
|
|
268
309
|
collection: string,
|
|
269
310
|
id: number,
|
|
270
311
|
) => Record<string, number | string | null>;
|
|
271
|
-
declare readVectorsById: (
|
|
272
|
-
|
|
312
|
+
declare readVectorsById: (
|
|
313
|
+
collection: string,
|
|
314
|
+
id: number,
|
|
315
|
+
) => Record<string, (number | null)[] | (string | null)[]>;
|
|
316
|
+
declare readSetsById: (
|
|
317
|
+
collection: string,
|
|
318
|
+
id: number,
|
|
319
|
+
) => Record<string, (number | null)[] | (string | null)[]>;
|
|
273
320
|
}
|
package/src/group-columns.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
|
-
import { type Pointer, ptr } from "bun:ffi";
|
|
1
|
+
import { CString, type Pointer, ptr, read, toArrayBuffer } from "bun:ffi";
|
|
2
2
|
import { check, QuiverError } from "./errors.ts";
|
|
3
3
|
import {
|
|
4
4
|
allocNativeFloat64,
|
|
5
5
|
allocNativeInt64,
|
|
6
6
|
allocNativePtrTable,
|
|
7
7
|
allocNativeStringArray,
|
|
8
|
+
allocPtrOut,
|
|
9
|
+
allocUint64Out,
|
|
10
|
+
decodeFloat64Array,
|
|
11
|
+
decodeInt64Array,
|
|
12
|
+
decodePtrArray,
|
|
13
|
+
decodeStringArray,
|
|
14
|
+
readPtrOut,
|
|
15
|
+
readUint64Out,
|
|
8
16
|
toCString,
|
|
9
17
|
} from "./ffi-helpers.ts";
|
|
10
|
-
import type
|
|
11
|
-
import {
|
|
18
|
+
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
19
|
+
import {
|
|
20
|
+
type Allocation,
|
|
21
|
+
DATA_TYPE_DATE_TIME,
|
|
22
|
+
DATA_TYPE_FLOAT,
|
|
23
|
+
DATA_TYPE_INTEGER,
|
|
24
|
+
DATA_TYPE_STRING,
|
|
25
|
+
} from "./types.ts";
|
|
12
26
|
|
|
13
27
|
/** Column-oriented group payload: one array of cells per column name, `null` for SQL NULL. */
|
|
14
28
|
export type GroupColumns = Record<string, (number | string | boolean | null)[]>;
|
|
15
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 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.
|
|
33
|
+
*/
|
|
34
|
+
export type TimeSeriesData = Record<string, (number | string | null)[]>;
|
|
35
|
+
|
|
16
36
|
/**
|
|
17
37
|
* The parallel-array signature every columnar group update C function shares
|
|
18
38
|
* (quiver_database_update_{time_series,vector,set}_group and their _by_label forms). The 4th
|
|
@@ -170,3 +190,123 @@ export function updateGroupColumns(
|
|
|
170
190
|
),
|
|
171
191
|
);
|
|
172
192
|
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The out-parameter signature every columnar group read C function shares
|
|
196
|
+
* (quiver_database_read_time_series_group, quiver_database_read_{vector,set}_group_by_id).
|
|
197
|
+
*/
|
|
198
|
+
type ColumnReadFn = (
|
|
199
|
+
db: NativePointer,
|
|
200
|
+
collection: Uint8Array,
|
|
201
|
+
group: Uint8Array,
|
|
202
|
+
id: bigint,
|
|
203
|
+
names: Uint8Array,
|
|
204
|
+
types: Uint8Array,
|
|
205
|
+
data: Uint8Array,
|
|
206
|
+
masks: Uint8Array,
|
|
207
|
+
columnCount: Uint8Array,
|
|
208
|
+
rowCount: Uint8Array,
|
|
209
|
+
) => number;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Call one of the columnar group read C functions, decode its typed arrays + per-cell mask into
|
|
213
|
+
* columns, and free the C result. The read-side twin of updateGroupColumns, shared by
|
|
214
|
+
* readTimeSeriesGroup (which returns the columns) and readVectorGroupById / readSetGroupById
|
|
215
|
+
* (which transpose them into rows). A masked cell is `null`; DATE_TIME cells stay ISO 8601 strings.
|
|
216
|
+
*/
|
|
217
|
+
export function readGroupColumns(
|
|
218
|
+
handle: NativePointer,
|
|
219
|
+
readGroup: ColumnReadFn,
|
|
220
|
+
collection: string,
|
|
221
|
+
group: string,
|
|
222
|
+
id: number,
|
|
223
|
+
): TimeSeriesData {
|
|
224
|
+
const collBuf = toCString(collection);
|
|
225
|
+
const grpBuf = toCString(group);
|
|
226
|
+
const outNames = allocPtrOut();
|
|
227
|
+
const outTypes = allocPtrOut();
|
|
228
|
+
const outData = allocPtrOut();
|
|
229
|
+
const outHasValue = allocPtrOut();
|
|
230
|
+
const outColCount = allocUint64Out();
|
|
231
|
+
const outRowCount = allocUint64Out();
|
|
232
|
+
|
|
233
|
+
check(
|
|
234
|
+
readGroup(
|
|
235
|
+
handle,
|
|
236
|
+
collBuf.buf,
|
|
237
|
+
grpBuf.buf,
|
|
238
|
+
BigInt(id),
|
|
239
|
+
outNames.buf,
|
|
240
|
+
outTypes.buf,
|
|
241
|
+
outData.buf,
|
|
242
|
+
outHasValue.buf,
|
|
243
|
+
outColCount.buf,
|
|
244
|
+
outRowCount.buf,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const colCount = readUint64Out(outColCount);
|
|
249
|
+
const rowCount = readUint64Out(outRowCount);
|
|
250
|
+
if (colCount === 0) return {};
|
|
251
|
+
|
|
252
|
+
const namesPtr = readPtrOut(outNames);
|
|
253
|
+
const typesPtr = readPtrOut(outTypes);
|
|
254
|
+
const dataPtr = readPtrOut(outData);
|
|
255
|
+
const hasValuePtr = readPtrOut(outHasValue);
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const colNames = decodeStringArray(namesPtr, colCount);
|
|
259
|
+
const typesAb = toArrayBuffer(typesPtr as Pointer, 0, colCount * 4);
|
|
260
|
+
const types = Array.from(new Int32Array(typesAb));
|
|
261
|
+
const dataPtrs = decodePtrArray(dataPtr, colCount);
|
|
262
|
+
const maskPtrs = decodePtrArray(hasValuePtr, colCount);
|
|
263
|
+
|
|
264
|
+
// Per-cell NULL mask: mask[r] === 0 means SQL NULL, surfaced as JS null. A time series'
|
|
265
|
+
// dimension column's mask is always all 1, so it stays dense.
|
|
266
|
+
const result: TimeSeriesData = {};
|
|
267
|
+
for (let c = 0; c < colCount; c++) {
|
|
268
|
+
const colName = colNames[c];
|
|
269
|
+
const maskPtr = maskPtrs[c];
|
|
270
|
+
const mask = maskPtr ? new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, rowCount)) : null;
|
|
271
|
+
switch (types[c]) {
|
|
272
|
+
case DATA_TYPE_INTEGER: {
|
|
273
|
+
const vals = decodeInt64Array(dataPtrs[c], rowCount);
|
|
274
|
+
result[colName] = mask ? vals.map((v, r) => (mask[r] ? v : null)) : vals;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case DATA_TYPE_FLOAT: {
|
|
278
|
+
const vals = decodeFloat64Array(dataPtrs[c], rowCount);
|
|
279
|
+
result[colName] = mask ? vals.map((v, r) => (mask[r] ? v : null)) : vals;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case DATA_TYPE_STRING:
|
|
283
|
+
case DATA_TYPE_DATE_TIME: {
|
|
284
|
+
// Read pointer-by-pointer, not with decodeStringArray, which turns a NULL char* into "":
|
|
285
|
+
// a masked-out or NULL cell must come back as null.
|
|
286
|
+
const base = dataPtrs[c];
|
|
287
|
+
const col: (string | null)[] = new Array(rowCount);
|
|
288
|
+
for (let r = 0; r < rowCount; r++) {
|
|
289
|
+
if (mask && !mask[r]) {
|
|
290
|
+
col[r] = null;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const strPtr = base ? read.ptr(base as Pointer, r * 8) : 0;
|
|
294
|
+
col[r] = strPtr === 0 ? null : new CString(strPtr as Pointer).toString();
|
|
295
|
+
}
|
|
296
|
+
result[colName] = col;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
} finally {
|
|
303
|
+
getSymbols().quiver_database_free_time_series_data(
|
|
304
|
+
namesPtr,
|
|
305
|
+
typesPtr,
|
|
306
|
+
dataPtr,
|
|
307
|
+
hasValuePtr,
|
|
308
|
+
BigInt(colCount),
|
|
309
|
+
BigInt(rowCount),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,11 +11,10 @@ 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
|
+
export type { GroupColumns, TimeSeriesData } from "./group-columns.ts";
|
|
15
15
|
export { LUA_DB_API_REFERENCE } from "./lua-api.ts";
|
|
16
16
|
export { LuaRunner } from "./lua-runner.ts";
|
|
17
17
|
export type { GroupMetadata, ScalarMetadata } from "./metadata.ts";
|
|
18
|
-
export type { TimeSeriesData } from "./time-series.ts";
|
|
19
18
|
export type {
|
|
20
19
|
ArrayValue,
|
|
21
20
|
DatabaseOptions,
|
package/src/loader.ts
CHANGED
|
@@ -73,29 +73,34 @@ 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
|
+
quiver_database_read_vector_group_by_id: {
|
|
89
|
+
args: [P, BUF, BUF, I64, P, P, P, P, P, P],
|
|
90
|
+
returns: I32,
|
|
91
|
+
},
|
|
92
|
+
quiver_database_read_set_group_by_id: {
|
|
93
|
+
args: [P, BUF, BUF, I64, P, P, P, P, P, P],
|
|
94
|
+
returns: I32,
|
|
95
|
+
},
|
|
88
96
|
quiver_database_read_element_ids: { args: [P, BUF, P, P], returns: I32 },
|
|
89
97
|
quiver_database_number_of_elements: { args: [P, BUF, P], returns: I32 },
|
|
90
98
|
} as const;
|
|
91
99
|
|
|
92
100
|
const querySymbols = {
|
|
93
|
-
quiver_database_query_string: { args: [P, BUF, P, P], returns: I32 },
|
|
94
|
-
quiver_database_query_integer: { args: [P, BUF, P, P], returns: I32 },
|
|
95
|
-
quiver_database_query_float: { args: [P, BUF, P, P], returns: I32 },
|
|
96
|
-
quiver_database_query_string_params: { args: [P, BUF, P, P, USIZE, P, P], returns: I32 },
|
|
97
|
-
quiver_database_query_integer_params: { args: [P, BUF, P, P, USIZE, P, P], returns: I32 },
|
|
98
|
-
quiver_database_query_float_params: { args: [P, BUF, P, P, USIZE, P, P], returns: I32 },
|
|
101
|
+
quiver_database_query_string: { args: [P, BUF, P, P, USIZE, P, P], returns: I32 },
|
|
102
|
+
quiver_database_query_integer: { args: [P, BUF, P, P, USIZE, P, P], returns: I32 },
|
|
103
|
+
quiver_database_query_float: { args: [P, BUF, P, P, USIZE, P, P], returns: I32 },
|
|
99
104
|
} as const;
|
|
100
105
|
|
|
101
106
|
const transactionSymbols = {
|
|
@@ -131,7 +136,7 @@ const timeSeriesSymbols = {
|
|
|
131
136
|
args: [P, BUF, BUF, I64, P, P, P, P, P, P],
|
|
132
137
|
returns: I32,
|
|
133
138
|
},
|
|
134
|
-
quiver_database_read_time_series_row: { args: [P, BUF, BUF, BUF, BUF, P, P, P], returns: I32 },
|
|
139
|
+
quiver_database_read_time_series_row: { args: [P, BUF, BUF, BUF, BUF, P, P, P, P], returns: I32 },
|
|
135
140
|
quiver_database_upsert_time_series_row: {
|
|
136
141
|
args: [P, BUF, BUF, I64, P, P, P, USIZE],
|
|
137
142
|
returns: I32,
|
|
@@ -181,6 +186,7 @@ const freeSymbols = {
|
|
|
181
186
|
quiver_database_free_integer_array: { args: [P], returns: I32 },
|
|
182
187
|
quiver_database_free_float_array: { args: [P], returns: I32 },
|
|
183
188
|
quiver_database_free_mask: { args: [P], returns: I32 },
|
|
189
|
+
quiver_database_free_masks: { args: [P, USIZE], returns: I32 },
|
|
184
190
|
quiver_database_free_string_array: { args: [P, USIZE], returns: I32 },
|
|
185
191
|
quiver_database_free_string: { args: [P], returns: I32 },
|
|
186
192
|
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
|
|
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}, {
|
|
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,20 @@ 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,
|
|
380
|
-
db:read_sets_by_id(collection, id) -- { column = { v1,
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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. The result is keyed by column name, so when two vector
|
|
403
|
+
(or two set) groups share a column name, which a foreign key may, that name appears once and is
|
|
404
|
+
read from the group it resolves to (the one named after it, else the one whose table name sorts
|
|
405
|
+
first) — do not zip it with the other group's columns.
|
|
393
406
|
|
|
394
407
|
---
|
|
395
408
|
|
|
@@ -856,6 +869,6 @@ and \`unit\` default to \`""\`; \`labels\`, \`dimensions\`, \`dimension_sizes\`,
|
|
|
856
869
|
DateTime wrapper helpers (Lua uses ISO 8601 strings), boolean *reader* helpers (a stored flag reads
|
|
857
870
|
back as \`0\`/\`1\` — writing a boolean is supported), \`_by_id\` single-scalar variants (use the
|
|
858
871
|
composite by-id readers or the bulk readers instead), and the row-aligned whole-group readers the
|
|
859
|
-
other bindings have (hence the
|
|
872
|
+
other bindings have (hence the row-count caveat under composite by-id reads). Everything else
|
|
860
873
|
the native binding exposes — CRUD, reads, time series, metadata, query, CSV, and the
|
|
861
874
|
binary/expression subsystems — is documented above and callable.`;
|
package/src/query.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ptr } from "bun:ffi";
|
|
2
1
|
import { integerToBoolean } from "./boolean.ts";
|
|
3
2
|
import { Database } from "./database.ts";
|
|
4
3
|
import { check, QuiverError } from "./errors.ts";
|
|
@@ -22,11 +21,18 @@ import {
|
|
|
22
21
|
type QueryParam,
|
|
23
22
|
} from "./types.ts";
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Marshals query parameters into the C API's parallel type/value arrays. No parameters (omitted or
|
|
25
|
+
// empty) marshal to NULL pointers with a count of 0: the C API reads neither array then, and a
|
|
26
|
+
// zero-length TypedArray has no pointer in Bun (ptr() of one returns a TypeError).
|
|
27
|
+
function marshalParams(parameters?: QueryParam[]): {
|
|
28
|
+
types: Uint8Array | null;
|
|
29
|
+
values: Uint8Array | null;
|
|
30
|
+
count: bigint;
|
|
28
31
|
_keepalive: Allocation[];
|
|
29
32
|
} {
|
|
33
|
+
if (!parameters || parameters.length === 0)
|
|
34
|
+
return { types: null, values: null, count: 0n, _keepalive: [] };
|
|
35
|
+
|
|
30
36
|
const n = parameters.length;
|
|
31
37
|
const typesBuf = new Uint8Array(n * 4);
|
|
32
38
|
const typesDv = new DataView(typesBuf.buffer);
|
|
@@ -66,10 +72,7 @@ function marshalParams(parameters: QueryParam[]): {
|
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
const values: Allocation = { ptr: ptr(valuesBuf), buf: valuesBuf };
|
|
71
|
-
keepalive.push(types, values);
|
|
72
|
-
return { types, values, _keepalive: keepalive };
|
|
75
|
+
return { types: typesBuf, values: valuesBuf, count: BigInt(n), _keepalive: keepalive };
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
Database.prototype.queryString = function (
|
|
@@ -81,23 +84,19 @@ Database.prototype.queryString = function (
|
|
|
81
84
|
const sqlBuf = toCString(sql);
|
|
82
85
|
const outValue = allocPtrOut();
|
|
83
86
|
const outHasValue = new Uint8Array(4);
|
|
87
|
+
const params = marshalParams(parameters);
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
),
|
|
97
|
-
);
|
|
98
|
-
} else {
|
|
99
|
-
check(lib.quiver_database_query_string(this._handle, sqlBuf.buf, outValue.buf, outHasValue));
|
|
100
|
-
}
|
|
89
|
+
check(
|
|
90
|
+
lib.quiver_database_query_string(
|
|
91
|
+
this._handle,
|
|
92
|
+
sqlBuf.buf,
|
|
93
|
+
params.types,
|
|
94
|
+
params.values,
|
|
95
|
+
params.count,
|
|
96
|
+
outValue.buf,
|
|
97
|
+
outHasValue,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
101
100
|
|
|
102
101
|
if (new DataView(outHasValue.buffer).getInt32(0, true) === 0) return null;
|
|
103
102
|
const result = decodeStringFromBuf(outValue);
|
|
@@ -114,23 +113,19 @@ Database.prototype.queryInteger = function (
|
|
|
114
113
|
const sqlBuf = toCString(sql);
|
|
115
114
|
const outValue = new Uint8Array(8);
|
|
116
115
|
const outHasValue = new Uint8Array(4);
|
|
116
|
+
const params = marshalParams(parameters);
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
),
|
|
130
|
-
);
|
|
131
|
-
} else {
|
|
132
|
-
check(lib.quiver_database_query_integer(this._handle, sqlBuf.buf, outValue, outHasValue));
|
|
133
|
-
}
|
|
118
|
+
check(
|
|
119
|
+
lib.quiver_database_query_integer(
|
|
120
|
+
this._handle,
|
|
121
|
+
sqlBuf.buf,
|
|
122
|
+
params.types,
|
|
123
|
+
params.values,
|
|
124
|
+
params.count,
|
|
125
|
+
outValue,
|
|
126
|
+
outHasValue,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
134
129
|
|
|
135
130
|
if (new DataView(outHasValue.buffer).getInt32(0, true) === 0) return null;
|
|
136
131
|
return Number(new DataView(outValue.buffer).getBigInt64(0, true));
|
|
@@ -153,23 +148,19 @@ Database.prototype.queryFloat = function (
|
|
|
153
148
|
const sqlBuf = toCString(sql);
|
|
154
149
|
const outValue = new Uint8Array(8);
|
|
155
150
|
const outHasValue = new Uint8Array(4);
|
|
151
|
+
const params = marshalParams(parameters);
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
),
|
|
169
|
-
);
|
|
170
|
-
} else {
|
|
171
|
-
check(lib.quiver_database_query_float(this._handle, sqlBuf.buf, outValue, outHasValue));
|
|
172
|
-
}
|
|
153
|
+
check(
|
|
154
|
+
lib.quiver_database_query_float(
|
|
155
|
+
this._handle,
|
|
156
|
+
sqlBuf.buf,
|
|
157
|
+
params.types,
|
|
158
|
+
params.values,
|
|
159
|
+
params.count,
|
|
160
|
+
outValue,
|
|
161
|
+
outHasValue,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
173
164
|
|
|
174
165
|
if (new DataView(outHasValue.buffer).getInt32(0, true) === 0) return null;
|
|
175
166
|
return new DataView(outValue.buffer).getFloat64(0, true);
|
package/src/read.ts
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
decodeFloat64Array,
|
|
9
9
|
decodeInt64Array,
|
|
10
10
|
decodePtrArray,
|
|
11
|
-
decodeStringArray,
|
|
12
11
|
decodeStringFromBuf,
|
|
13
12
|
decodeUint64Array,
|
|
14
13
|
readPtrOut,
|
|
15
14
|
readUint64Out,
|
|
16
15
|
toCString,
|
|
17
16
|
} from "./ffi-helpers.ts";
|
|
17
|
+
import { readGroupColumns, type TimeSeriesData } from "./group-columns.ts";
|
|
18
18
|
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
19
19
|
|
|
20
20
|
// --- Scalar array reads ---
|
|
@@ -251,10 +251,11 @@ function readBulkIntegers(
|
|
|
251
251
|
fn: string,
|
|
252
252
|
collection: string,
|
|
253
253
|
attribute: string,
|
|
254
|
-
): number[][] {
|
|
254
|
+
): (number | null)[][] {
|
|
255
255
|
const collBuf = toCString(collection);
|
|
256
256
|
const attrBuf = toCString(attribute);
|
|
257
257
|
const outVectors = allocPtrOut();
|
|
258
|
+
const outMasks = allocPtrOut();
|
|
258
259
|
const outSizes = allocPtrOut();
|
|
259
260
|
const outCount = allocUint64Out();
|
|
260
261
|
check(
|
|
@@ -263,6 +264,7 @@ function readBulkIntegers(
|
|
|
263
264
|
collBuf.buf,
|
|
264
265
|
attrBuf.buf,
|
|
265
266
|
outVectors.buf,
|
|
267
|
+
outMasks.buf,
|
|
266
268
|
outSizes.buf,
|
|
267
269
|
outCount.buf,
|
|
268
270
|
),
|
|
@@ -270,14 +272,23 @@ function readBulkIntegers(
|
|
|
270
272
|
const count = readUint64Out(outCount);
|
|
271
273
|
if (count === 0) return [];
|
|
272
274
|
const vectorsPtr = readPtrOut(outVectors);
|
|
275
|
+
const masksPtr = readPtrOut(outMasks);
|
|
273
276
|
const sizesPtr = readPtrOut(outSizes);
|
|
274
277
|
const vectorPtrs = decodePtrArray(vectorsPtr, count);
|
|
278
|
+
const maskPtrs = decodePtrArray(masksPtr, count);
|
|
275
279
|
const sizes = decodeUint64Array(sizesPtr, count);
|
|
276
|
-
const result: number[][] = new Array(count);
|
|
280
|
+
const result: (number | null)[][] = new Array(count);
|
|
277
281
|
for (let i = 0; i < count; i++) {
|
|
278
|
-
|
|
282
|
+
if (sizes[i] === 0) {
|
|
283
|
+
result[i] = [];
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const values = decodeInt64Array(vectorPtrs[i], sizes[i]);
|
|
287
|
+
const mask = new Uint8Array(toArrayBuffer(maskPtrs[i] as Pointer, 0, sizes[i]));
|
|
288
|
+
result[i] = values.map((v, j) => (mask[j] ? v : null));
|
|
279
289
|
}
|
|
280
290
|
lib.quiver_database_free_integer_vectors(vectorsPtr, sizesPtr, BigInt(count));
|
|
291
|
+
lib.quiver_database_free_masks(masksPtr, BigInt(count));
|
|
281
292
|
return result;
|
|
282
293
|
}
|
|
283
294
|
|
|
@@ -287,10 +298,11 @@ function readBulkFloats(
|
|
|
287
298
|
fn: string,
|
|
288
299
|
collection: string,
|
|
289
300
|
attribute: string,
|
|
290
|
-
): number[][] {
|
|
301
|
+
): (number | null)[][] {
|
|
291
302
|
const collBuf = toCString(collection);
|
|
292
303
|
const attrBuf = toCString(attribute);
|
|
293
304
|
const outVectors = allocPtrOut();
|
|
305
|
+
const outMasks = allocPtrOut();
|
|
294
306
|
const outSizes = allocPtrOut();
|
|
295
307
|
const outCount = allocUint64Out();
|
|
296
308
|
check(
|
|
@@ -299,6 +311,7 @@ function readBulkFloats(
|
|
|
299
311
|
collBuf.buf,
|
|
300
312
|
attrBuf.buf,
|
|
301
313
|
outVectors.buf,
|
|
314
|
+
outMasks.buf,
|
|
302
315
|
outSizes.buf,
|
|
303
316
|
outCount.buf,
|
|
304
317
|
),
|
|
@@ -306,14 +319,23 @@ function readBulkFloats(
|
|
|
306
319
|
const count = readUint64Out(outCount);
|
|
307
320
|
if (count === 0) return [];
|
|
308
321
|
const vectorsPtr = readPtrOut(outVectors);
|
|
322
|
+
const masksPtr = readPtrOut(outMasks);
|
|
309
323
|
const sizesPtr = readPtrOut(outSizes);
|
|
310
324
|
const vectorPtrs = decodePtrArray(vectorsPtr, count);
|
|
325
|
+
const maskPtrs = decodePtrArray(masksPtr, count);
|
|
311
326
|
const sizes = decodeUint64Array(sizesPtr, count);
|
|
312
|
-
const result: number[][] = new Array(count);
|
|
327
|
+
const result: (number | null)[][] = new Array(count);
|
|
313
328
|
for (let i = 0; i < count; i++) {
|
|
314
|
-
|
|
329
|
+
if (sizes[i] === 0) {
|
|
330
|
+
result[i] = [];
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const values = decodeFloat64Array(vectorPtrs[i], sizes[i]);
|
|
334
|
+
const mask = new Uint8Array(toArrayBuffer(maskPtrs[i] as Pointer, 0, sizes[i]));
|
|
335
|
+
result[i] = values.map((v, j) => (mask[j] ? v : null));
|
|
315
336
|
}
|
|
316
337
|
lib.quiver_database_free_float_vectors(vectorsPtr, sizesPtr, BigInt(count));
|
|
338
|
+
lib.quiver_database_free_masks(masksPtr, BigInt(count));
|
|
317
339
|
return result;
|
|
318
340
|
}
|
|
319
341
|
|
|
@@ -323,7 +345,7 @@ function readBulkStrings(
|
|
|
323
345
|
fn: string,
|
|
324
346
|
collection: string,
|
|
325
347
|
attribute: string,
|
|
326
|
-
): string[][] {
|
|
348
|
+
): (string | null)[][] {
|
|
327
349
|
const collBuf = toCString(collection);
|
|
328
350
|
const attrBuf = toCString(attribute);
|
|
329
351
|
const outVectors = allocPtrOut();
|
|
@@ -345,9 +367,16 @@ function readBulkStrings(
|
|
|
345
367
|
const sizesPtr = readPtrOut(outSizes);
|
|
346
368
|
const vectorPtrs = decodePtrArray(vectorsPtr, count);
|
|
347
369
|
const sizes = decodeUint64Array(sizesPtr, count);
|
|
348
|
-
const result: string[][] = new Array(count);
|
|
370
|
+
const result: (string | null)[][] = new Array(count);
|
|
349
371
|
for (let i = 0; i < count; i++) {
|
|
350
|
-
|
|
372
|
+
// A NULL cell is a NULL char* entry, so decode the pointer array and map NULL -> null
|
|
373
|
+
// (decodeStringArray would construct "" from a NULL pointer instead).
|
|
374
|
+
result[i] =
|
|
375
|
+
sizes[i] === 0
|
|
376
|
+
? []
|
|
377
|
+
: decodePtrArray(vectorPtrs[i], sizes[i]).map((cell) =>
|
|
378
|
+
cell === null ? null : new CString(cell).toString(),
|
|
379
|
+
);
|
|
351
380
|
}
|
|
352
381
|
lib.quiver_database_free_string_vectors(vectorsPtr, sizesPtr, BigInt(count));
|
|
353
382
|
return result;
|
|
@@ -357,7 +386,7 @@ Database.prototype.readVectorIntegers = function (
|
|
|
357
386
|
this: Database,
|
|
358
387
|
collection: string,
|
|
359
388
|
attribute: string,
|
|
360
|
-
): number[][] {
|
|
389
|
+
): (number | null)[][] {
|
|
361
390
|
return readBulkIntegers(
|
|
362
391
|
getSymbols(),
|
|
363
392
|
this._handle,
|
|
@@ -366,15 +395,12 @@ Database.prototype.readVectorIntegers = function (
|
|
|
366
395
|
attribute,
|
|
367
396
|
);
|
|
368
397
|
};
|
|
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
|
-
*/
|
|
398
|
+
/** One entry per element; within an entry a SQL NULL cell is `null`. */
|
|
373
399
|
Database.prototype.readVectorBooleans = function (
|
|
374
400
|
this: Database,
|
|
375
401
|
collection: string,
|
|
376
402
|
attribute: string,
|
|
377
|
-
): boolean[][] {
|
|
403
|
+
): (boolean | null)[][] {
|
|
378
404
|
return this.readVectorIntegers(collection, attribute).map((values) =>
|
|
379
405
|
values.map((value) => integerToBoolean(value, collection, attribute)),
|
|
380
406
|
);
|
|
@@ -383,7 +409,7 @@ Database.prototype.readVectorFloats = function (
|
|
|
383
409
|
this: Database,
|
|
384
410
|
collection: string,
|
|
385
411
|
attribute: string,
|
|
386
|
-
): number[][] {
|
|
412
|
+
): (number | null)[][] {
|
|
387
413
|
return readBulkFloats(
|
|
388
414
|
getSymbols(),
|
|
389
415
|
this._handle,
|
|
@@ -396,7 +422,7 @@ Database.prototype.readVectorStrings = function (
|
|
|
396
422
|
this: Database,
|
|
397
423
|
collection: string,
|
|
398
424
|
attribute: string,
|
|
399
|
-
): string[][] {
|
|
425
|
+
): (string | null)[][] {
|
|
400
426
|
return readBulkStrings(
|
|
401
427
|
getSymbols(),
|
|
402
428
|
this._handle,
|
|
@@ -409,7 +435,7 @@ Database.prototype.readSetIntegers = function (
|
|
|
409
435
|
this: Database,
|
|
410
436
|
collection: string,
|
|
411
437
|
attribute: string,
|
|
412
|
-
): number[][] {
|
|
438
|
+
): (number | null)[][] {
|
|
413
439
|
return readBulkIntegers(
|
|
414
440
|
getSymbols(),
|
|
415
441
|
this._handle,
|
|
@@ -418,12 +444,12 @@ Database.prototype.readSetIntegers = function (
|
|
|
418
444
|
attribute,
|
|
419
445
|
);
|
|
420
446
|
};
|
|
421
|
-
/** Same
|
|
447
|
+
/** Same contract as `readVectorBooleans`: one entry per element, and a NULL cell is `null`. */
|
|
422
448
|
Database.prototype.readSetBooleans = function (
|
|
423
449
|
this: Database,
|
|
424
450
|
collection: string,
|
|
425
451
|
attribute: string,
|
|
426
|
-
): boolean[][] {
|
|
452
|
+
): (boolean | null)[][] {
|
|
427
453
|
return this.readSetIntegers(collection, attribute).map((values) =>
|
|
428
454
|
values.map((value) => integerToBoolean(value, collection, attribute)),
|
|
429
455
|
);
|
|
@@ -432,7 +458,7 @@ Database.prototype.readSetFloats = function (
|
|
|
432
458
|
this: Database,
|
|
433
459
|
collection: string,
|
|
434
460
|
attribute: string,
|
|
435
|
-
): number[][] {
|
|
461
|
+
): (number | null)[][] {
|
|
436
462
|
return readBulkFloats(
|
|
437
463
|
getSymbols(),
|
|
438
464
|
this._handle,
|
|
@@ -445,7 +471,7 @@ Database.prototype.readSetStrings = function (
|
|
|
445
471
|
this: Database,
|
|
446
472
|
collection: string,
|
|
447
473
|
attribute: string,
|
|
448
|
-
): string[][] {
|
|
474
|
+
): (string | null)[][] {
|
|
449
475
|
return readBulkStrings(
|
|
450
476
|
getSymbols(),
|
|
451
477
|
this._handle,
|
|
@@ -464,10 +490,11 @@ function readByIdIntegers(
|
|
|
464
490
|
collection: string,
|
|
465
491
|
attribute: string,
|
|
466
492
|
id: number,
|
|
467
|
-
): number[] {
|
|
493
|
+
): (number | null)[] {
|
|
468
494
|
const collBuf = toCString(collection);
|
|
469
495
|
const attrBuf = toCString(attribute);
|
|
470
496
|
const outValues = allocPtrOut();
|
|
497
|
+
const outMask = allocPtrOut();
|
|
471
498
|
const outCount = allocUint64Out();
|
|
472
499
|
check(
|
|
473
500
|
(lib as Record<string, Function>)[fn](
|
|
@@ -476,14 +503,19 @@ function readByIdIntegers(
|
|
|
476
503
|
attrBuf.buf,
|
|
477
504
|
BigInt(id),
|
|
478
505
|
outValues.buf,
|
|
506
|
+
outMask.buf,
|
|
479
507
|
outCount.buf,
|
|
480
508
|
),
|
|
481
509
|
);
|
|
482
510
|
const count = readUint64Out(outCount);
|
|
483
511
|
if (count === 0) return [];
|
|
484
512
|
const arrPtr = readPtrOut(outValues);
|
|
485
|
-
const
|
|
513
|
+
const maskPtr = readPtrOut(outMask);
|
|
514
|
+
const values = decodeInt64Array(arrPtr, count);
|
|
515
|
+
const mask = new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, count));
|
|
516
|
+
const result = values.map((v, i) => (mask[i] ? v : null));
|
|
486
517
|
lib.quiver_database_free_integer_array(arrPtr);
|
|
518
|
+
lib.quiver_database_free_mask(maskPtr);
|
|
487
519
|
return result;
|
|
488
520
|
}
|
|
489
521
|
|
|
@@ -494,10 +526,11 @@ function readByIdFloats(
|
|
|
494
526
|
collection: string,
|
|
495
527
|
attribute: string,
|
|
496
528
|
id: number,
|
|
497
|
-
): number[] {
|
|
529
|
+
): (number | null)[] {
|
|
498
530
|
const collBuf = toCString(collection);
|
|
499
531
|
const attrBuf = toCString(attribute);
|
|
500
532
|
const outValues = allocPtrOut();
|
|
533
|
+
const outMask = allocPtrOut();
|
|
501
534
|
const outCount = allocUint64Out();
|
|
502
535
|
check(
|
|
503
536
|
(lib as Record<string, Function>)[fn](
|
|
@@ -506,14 +539,19 @@ function readByIdFloats(
|
|
|
506
539
|
attrBuf.buf,
|
|
507
540
|
BigInt(id),
|
|
508
541
|
outValues.buf,
|
|
542
|
+
outMask.buf,
|
|
509
543
|
outCount.buf,
|
|
510
544
|
),
|
|
511
545
|
);
|
|
512
546
|
const count = readUint64Out(outCount);
|
|
513
547
|
if (count === 0) return [];
|
|
514
548
|
const arrPtr = readPtrOut(outValues);
|
|
515
|
-
const
|
|
549
|
+
const maskPtr = readPtrOut(outMask);
|
|
550
|
+
const values = decodeFloat64Array(arrPtr, count);
|
|
551
|
+
const mask = new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, count));
|
|
552
|
+
const result = values.map((v, i) => (mask[i] ? v : null));
|
|
516
553
|
lib.quiver_database_free_float_array(arrPtr);
|
|
554
|
+
lib.quiver_database_free_mask(maskPtr);
|
|
517
555
|
return result;
|
|
518
556
|
}
|
|
519
557
|
|
|
@@ -524,7 +562,7 @@ function readByIdStrings(
|
|
|
524
562
|
collection: string,
|
|
525
563
|
attribute: string,
|
|
526
564
|
id: number,
|
|
527
|
-
): string[] {
|
|
565
|
+
): (string | null)[] {
|
|
528
566
|
const collBuf = toCString(collection);
|
|
529
567
|
const attrBuf = toCString(attribute);
|
|
530
568
|
const outValues = allocPtrOut();
|
|
@@ -542,7 +580,11 @@ function readByIdStrings(
|
|
|
542
580
|
const count = readUint64Out(outCount);
|
|
543
581
|
if (count === 0) return [];
|
|
544
582
|
const arrPtr = readPtrOut(outValues);
|
|
545
|
-
|
|
583
|
+
// A NULL cell is a NULL char* entry, so decode the pointer array and map NULL -> null
|
|
584
|
+
// (decodeStringArray would construct "" from a NULL pointer instead).
|
|
585
|
+
const result = decodePtrArray(arrPtr, count).map((cell) =>
|
|
586
|
+
cell === null ? null : new CString(cell).toString(),
|
|
587
|
+
);
|
|
546
588
|
lib.quiver_database_free_string_array(arrPtr, BigInt(count));
|
|
547
589
|
return result;
|
|
548
590
|
}
|
|
@@ -552,7 +594,7 @@ Database.prototype.readVectorIntegersById = function (
|
|
|
552
594
|
collection: string,
|
|
553
595
|
attribute: string,
|
|
554
596
|
id: number,
|
|
555
|
-
): number[] {
|
|
597
|
+
): (number | null)[] {
|
|
556
598
|
return readByIdIntegers(
|
|
557
599
|
getSymbols(),
|
|
558
600
|
this._handle,
|
|
@@ -567,7 +609,7 @@ Database.prototype.readVectorBooleansById = function (
|
|
|
567
609
|
collection: string,
|
|
568
610
|
attribute: string,
|
|
569
611
|
id: number,
|
|
570
|
-
): boolean[] {
|
|
612
|
+
): (boolean | null)[] {
|
|
571
613
|
return this.readVectorIntegersById(collection, attribute, id).map((value) =>
|
|
572
614
|
integerToBoolean(value, collection, attribute),
|
|
573
615
|
);
|
|
@@ -577,7 +619,7 @@ Database.prototype.readVectorFloatsById = function (
|
|
|
577
619
|
collection: string,
|
|
578
620
|
attribute: string,
|
|
579
621
|
id: number,
|
|
580
|
-
): number[] {
|
|
622
|
+
): (number | null)[] {
|
|
581
623
|
return readByIdFloats(
|
|
582
624
|
getSymbols(),
|
|
583
625
|
this._handle,
|
|
@@ -592,7 +634,7 @@ Database.prototype.readVectorStringsById = function (
|
|
|
592
634
|
collection: string,
|
|
593
635
|
attribute: string,
|
|
594
636
|
id: number,
|
|
595
|
-
): string[] {
|
|
637
|
+
): (string | null)[] {
|
|
596
638
|
return readByIdStrings(
|
|
597
639
|
getSymbols(),
|
|
598
640
|
this._handle,
|
|
@@ -607,7 +649,7 @@ Database.prototype.readSetIntegersById = function (
|
|
|
607
649
|
collection: string,
|
|
608
650
|
attribute: string,
|
|
609
651
|
id: number,
|
|
610
|
-
): number[] {
|
|
652
|
+
): (number | null)[] {
|
|
611
653
|
return readByIdIntegers(
|
|
612
654
|
getSymbols(),
|
|
613
655
|
this._handle,
|
|
@@ -622,7 +664,7 @@ Database.prototype.readSetBooleansById = function (
|
|
|
622
664
|
collection: string,
|
|
623
665
|
attribute: string,
|
|
624
666
|
id: number,
|
|
625
|
-
): boolean[] {
|
|
667
|
+
): (boolean | null)[] {
|
|
626
668
|
return this.readSetIntegersById(collection, attribute, id).map((value) =>
|
|
627
669
|
integerToBoolean(value, collection, attribute),
|
|
628
670
|
);
|
|
@@ -632,7 +674,7 @@ Database.prototype.readSetFloatsById = function (
|
|
|
632
674
|
collection: string,
|
|
633
675
|
attribute: string,
|
|
634
676
|
id: number,
|
|
635
|
-
): number[] {
|
|
677
|
+
): (number | null)[] {
|
|
636
678
|
return readByIdFloats(
|
|
637
679
|
getSymbols(),
|
|
638
680
|
this._handle,
|
|
@@ -647,7 +689,7 @@ Database.prototype.readSetStringsById = function (
|
|
|
647
689
|
collection: string,
|
|
648
690
|
attribute: string,
|
|
649
691
|
id: number,
|
|
650
|
-
): string[] {
|
|
692
|
+
): (string | null)[] {
|
|
651
693
|
return readByIdStrings(
|
|
652
694
|
getSymbols(),
|
|
653
695
|
this._handle,
|
|
@@ -657,3 +699,58 @@ Database.prototype.readSetStringsById = function (
|
|
|
657
699
|
id,
|
|
658
700
|
);
|
|
659
701
|
};
|
|
702
|
+
|
|
703
|
+
// --- Whole-group reads ---
|
|
704
|
+
|
|
705
|
+
/** One record per row, from decoded columns that all share one length. */
|
|
706
|
+
function columnsToRows(columns: TimeSeriesData): Record<string, number | string | null>[] {
|
|
707
|
+
const names = Object.keys(columns);
|
|
708
|
+
if (names.length === 0) return [];
|
|
709
|
+
return columns[names[0]].map((_, r) =>
|
|
710
|
+
Object.fromEntries(names.map((name) => [name, columns[name][r]])),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Read an element's vector group as one record per row, in vector_index order. One native read of
|
|
716
|
+
* the named group's own table: a SQL NULL cell is `null` in its row, and a column name another
|
|
717
|
+
* group shares still reads this group (a per-column reader resolves the name). DATE_TIME cells
|
|
718
|
+
* stay ISO 8601 strings.
|
|
719
|
+
*/
|
|
720
|
+
Database.prototype.readVectorGroupById = function (
|
|
721
|
+
this: Database,
|
|
722
|
+
collection: string,
|
|
723
|
+
group: string,
|
|
724
|
+
id: number,
|
|
725
|
+
): Record<string, number | string | null>[] {
|
|
726
|
+
return columnsToRows(
|
|
727
|
+
readGroupColumns(
|
|
728
|
+
this._handle,
|
|
729
|
+
getSymbols().quiver_database_read_vector_group_by_id,
|
|
730
|
+
collection,
|
|
731
|
+
group,
|
|
732
|
+
id,
|
|
733
|
+
),
|
|
734
|
+
);
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Set-group counterpart of readVectorGroupById. Row order is consistent across every reader of
|
|
739
|
+
* the group, otherwise unspecified.
|
|
740
|
+
*/
|
|
741
|
+
Database.prototype.readSetGroupById = function (
|
|
742
|
+
this: Database,
|
|
743
|
+
collection: string,
|
|
744
|
+
group: string,
|
|
745
|
+
id: number,
|
|
746
|
+
): Record<string, number | string | null>[] {
|
|
747
|
+
return columnsToRows(
|
|
748
|
+
readGroupColumns(
|
|
749
|
+
this._handle,
|
|
750
|
+
getSymbols().quiver_database_read_set_group_by_id,
|
|
751
|
+
collection,
|
|
752
|
+
group,
|
|
753
|
+
id,
|
|
754
|
+
),
|
|
755
|
+
);
|
|
756
|
+
};
|
package/src/time-series.ts
CHANGED
|
@@ -11,23 +11,19 @@ import {
|
|
|
11
11
|
allocUint64Out,
|
|
12
12
|
decodeFloat64Array,
|
|
13
13
|
decodeInt64Array,
|
|
14
|
-
decodePtrArray,
|
|
15
14
|
decodeStringArray,
|
|
16
15
|
readPtrOut,
|
|
17
16
|
readUint64Out,
|
|
18
17
|
toCString,
|
|
19
18
|
} from "./ffi-helpers.ts";
|
|
20
|
-
import { type GroupColumns, updateGroupColumns } from "./group-columns.ts";
|
|
21
|
-
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
22
19
|
import {
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} from "./
|
|
29
|
-
|
|
30
|
-
export type TimeSeriesData = Record<string, (number | string | null)[]>;
|
|
20
|
+
type GroupColumns,
|
|
21
|
+
readGroupColumns,
|
|
22
|
+
type TimeSeriesData,
|
|
23
|
+
updateGroupColumns,
|
|
24
|
+
} from "./group-columns.ts";
|
|
25
|
+
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
26
|
+
import { type Allocation, DATA_TYPE_FLOAT, DATA_TYPE_INTEGER, DATA_TYPE_STRING } from "./types.ts";
|
|
31
27
|
|
|
32
28
|
Database.prototype.readTimeSeriesGroup = function (
|
|
33
29
|
this: Database,
|
|
@@ -35,93 +31,13 @@ Database.prototype.readTimeSeriesGroup = function (
|
|
|
35
31
|
group: string,
|
|
36
32
|
id: number,
|
|
37
33
|
): TimeSeriesData {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const outHasValue = allocPtrOut();
|
|
45
|
-
const outColCount = allocUint64Out();
|
|
46
|
-
const outRowCount = allocUint64Out();
|
|
47
|
-
|
|
48
|
-
check(
|
|
49
|
-
lib.quiver_database_read_time_series_group(
|
|
50
|
-
this._handle,
|
|
51
|
-
collBuf.buf,
|
|
52
|
-
grpBuf.buf,
|
|
53
|
-
BigInt(id),
|
|
54
|
-
outNames.buf,
|
|
55
|
-
outTypes.buf,
|
|
56
|
-
outData.buf,
|
|
57
|
-
outHasValue.buf,
|
|
58
|
-
outColCount.buf,
|
|
59
|
-
outRowCount.buf,
|
|
60
|
-
),
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const colCount = readUint64Out(outColCount);
|
|
64
|
-
const rowCount = readUint64Out(outRowCount);
|
|
65
|
-
if (colCount === 0) return {};
|
|
66
|
-
|
|
67
|
-
const namesPtr = readPtrOut(outNames);
|
|
68
|
-
const typesPtr = readPtrOut(outTypes);
|
|
69
|
-
const dataPtr = readPtrOut(outData);
|
|
70
|
-
const hasValuePtr = readPtrOut(outHasValue);
|
|
71
|
-
|
|
72
|
-
const colNames = decodeStringArray(namesPtr, colCount);
|
|
73
|
-
const typesAb = toArrayBuffer(typesPtr as Pointer, 0, colCount * 4);
|
|
74
|
-
const types = Array.from(new Int32Array(typesAb));
|
|
75
|
-
const dataPtrs = decodePtrArray(dataPtr, colCount);
|
|
76
|
-
const maskPtrs = decodePtrArray(hasValuePtr, colCount);
|
|
77
|
-
|
|
78
|
-
// Per-cell NULL mask: mask[r] === 0 means SQL NULL, surfaced as JS null. The
|
|
79
|
-
// dimension column's mask is always all 1, so it stays dense.
|
|
80
|
-
const result: TimeSeriesData = {};
|
|
81
|
-
for (let c = 0; c < colCount; c++) {
|
|
82
|
-
const colName = colNames[c];
|
|
83
|
-
const maskPtr = maskPtrs[c];
|
|
84
|
-
const mask = maskPtr ? new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, rowCount)) : null;
|
|
85
|
-
switch (types[c]) {
|
|
86
|
-
case DATA_TYPE_INTEGER: {
|
|
87
|
-
const vals = decodeInt64Array(dataPtrs[c], rowCount);
|
|
88
|
-
result[colName] = mask ? vals.map((v, r) => (mask[r] ? v : null)) : vals;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
case DATA_TYPE_FLOAT: {
|
|
92
|
-
const vals = decodeFloat64Array(dataPtrs[c], rowCount);
|
|
93
|
-
result[colName] = mask ? vals.map((v, r) => (mask[r] ? v : null)) : vals;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
case DATA_TYPE_STRING:
|
|
97
|
-
case DATA_TYPE_DATE_TIME: {
|
|
98
|
-
// Read pointer-by-pointer (not decodeStringArray): a masked-out cell is a
|
|
99
|
-
// NULL char* that CString cannot construct from.
|
|
100
|
-
const base = dataPtrs[c];
|
|
101
|
-
const col: (string | null)[] = new Array(rowCount);
|
|
102
|
-
for (let r = 0; r < rowCount; r++) {
|
|
103
|
-
if (mask && !mask[r]) {
|
|
104
|
-
col[r] = null;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const strPtr = base ? read.ptr(base as Pointer, r * 8) : 0;
|
|
108
|
-
col[r] = strPtr === 0 ? null : new CString(strPtr as Pointer).toString();
|
|
109
|
-
}
|
|
110
|
-
result[colName] = col;
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
lib.quiver_database_free_time_series_data(
|
|
117
|
-
namesPtr,
|
|
118
|
-
typesPtr,
|
|
119
|
-
dataPtr,
|
|
120
|
-
hasValuePtr,
|
|
121
|
-
BigInt(colCount),
|
|
122
|
-
BigInt(rowCount),
|
|
34
|
+
return readGroupColumns(
|
|
35
|
+
this._handle,
|
|
36
|
+
getSymbols().quiver_database_read_time_series_group,
|
|
37
|
+
collection,
|
|
38
|
+
group,
|
|
39
|
+
id,
|
|
123
40
|
);
|
|
124
|
-
return result;
|
|
125
41
|
};
|
|
126
42
|
|
|
127
43
|
Database.prototype.readTimeSeriesRow = function (
|
|
@@ -138,6 +54,7 @@ Database.prototype.readTimeSeriesRow = function (
|
|
|
138
54
|
const dtBuf = toCString(dateTime);
|
|
139
55
|
const outDataType = new Uint8Array(4);
|
|
140
56
|
const outValues = allocPtrOut();
|
|
57
|
+
const outMask = allocPtrOut();
|
|
141
58
|
const outCount = allocUint64Out();
|
|
142
59
|
|
|
143
60
|
check(
|
|
@@ -149,6 +66,7 @@ Database.prototype.readTimeSeriesRow = function (
|
|
|
149
66
|
dtBuf.buf,
|
|
150
67
|
outDataType,
|
|
151
68
|
outValues.buf,
|
|
69
|
+
outMask.buf,
|
|
152
70
|
outCount.buf,
|
|
153
71
|
),
|
|
154
72
|
);
|
|
@@ -157,26 +75,33 @@ Database.prototype.readTimeSeriesRow = function (
|
|
|
157
75
|
const valuesPtr = readPtrOut(outValues);
|
|
158
76
|
if (count === 0 || !valuesPtr) return [];
|
|
159
77
|
|
|
78
|
+
// mask[i] === 0: no data at or before dateTime; the data slot is a placeholder, never read.
|
|
79
|
+
const maskPtr = readPtrOut(outMask);
|
|
80
|
+
const mask = new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, count));
|
|
160
81
|
const dataType = new DataView(outDataType.buffer).getInt32(0, true);
|
|
161
82
|
switch (dataType) {
|
|
162
83
|
case DATA_TYPE_INTEGER: {
|
|
163
|
-
const result = decodeInt64Array(valuesPtr, count);
|
|
84
|
+
const result = decodeInt64Array(valuesPtr, count).map((v, i) => (mask[i] ? v : null));
|
|
164
85
|
lib.quiver_database_free_integer_array(valuesPtr);
|
|
86
|
+
lib.quiver_database_free_mask(maskPtr);
|
|
165
87
|
return result;
|
|
166
88
|
}
|
|
167
89
|
case DATA_TYPE_FLOAT: {
|
|
168
|
-
const result = decodeFloat64Array(valuesPtr, count);
|
|
90
|
+
const result = decodeFloat64Array(valuesPtr, count).map((v, i) => (mask[i] ? v : null));
|
|
169
91
|
lib.quiver_database_free_float_array(valuesPtr);
|
|
92
|
+
lib.quiver_database_free_mask(maskPtr);
|
|
170
93
|
return result;
|
|
171
94
|
}
|
|
172
95
|
default: {
|
|
173
|
-
// STRING or DATE_TIME;
|
|
96
|
+
// STRING or DATE_TIME; never build a CString from a masked-out (NULL) pointer
|
|
174
97
|
const result: (string | null)[] = new Array(count);
|
|
175
98
|
for (let i = 0; i < count; i++) {
|
|
176
|
-
|
|
177
|
-
|
|
99
|
+
result[i] = mask[i]
|
|
100
|
+
? new CString(read.ptr(valuesPtr as Pointer, i * 8) as Pointer).toString()
|
|
101
|
+
: null;
|
|
178
102
|
}
|
|
179
103
|
lib.quiver_database_free_string_array(valuesPtr, BigInt(count));
|
|
104
|
+
lib.quiver_database_free_mask(maskPtr);
|
|
180
105
|
return result;
|
|
181
106
|
}
|
|
182
107
|
}
|
|
@@ -426,7 +351,12 @@ Database.prototype.updateTimeSeriesFiles = function (
|
|
|
426
351
|
const lib = getSymbols();
|
|
427
352
|
const collBuf = toCString(collection);
|
|
428
353
|
const entries = Object.entries(data);
|
|
429
|
-
|
|
354
|
+
// An empty map still reaches the core, which validates the collection and its files table
|
|
355
|
+
// before the no-op. null tables, never zero-length buffers (same as updateGroupColumns).
|
|
356
|
+
if (entries.length === 0) {
|
|
357
|
+
check(lib.quiver_database_update_time_series_files(this._handle, collBuf.buf, null, null, 0n));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
430
360
|
const keepalive: Allocation[] = [];
|
|
431
361
|
|
|
432
362
|
const colNames = entries.map(([name]) => name);
|