quiverdb 0.12.4 → 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 +8 -3
- 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/mod.ts +1 -13
- package/package.json +1 -1
- package/src/create.ts +27 -28
- package/src/database.ts +11 -2
- package/src/group-columns.ts +190 -21
- package/src/index.ts +6 -2
- package/src/loader.ts +12 -7
- package/src/lua-api.ts +4 -1
- package/src/query.ts +52 -56
- package/src/read.ts +56 -0
- package/src/time-series.ts +33 -103
- package/src/types.ts +2 -1
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
|
|
|
@@ -133,8 +135,8 @@ bun run example.ts
|
|
|
133
135
|
- `queryBoolean(sql, parameters?)` -- Query returning an INTEGER-backed boolean or null
|
|
134
136
|
- `queryFloat(sql, parameters?)` -- Query returning float or null
|
|
135
137
|
|
|
136
|
-
Parameters are passed as an array of `number | boolean | string | null` (a `
|
|
137
|
-
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).
|
|
138
140
|
|
|
139
141
|
### Transaction
|
|
140
142
|
|
|
@@ -180,12 +182,15 @@ Exported types available for TypeScript consumers:
|
|
|
180
182
|
- `ArrayValue` -- `number[] | bigint[] | string[]`
|
|
181
183
|
- `Value` -- `ScalarValue | ArrayValue`
|
|
182
184
|
- `ElementData` -- `Record<string, Value | undefined>`
|
|
183
|
-
- `QueryParam` -- `number | string | null`
|
|
185
|
+
- `QueryParam` -- `number | bigint | boolean | string | null`
|
|
184
186
|
- `QuiverError` -- Error class for all Quiver operations
|
|
185
187
|
- `ScalarMetadata` -- Scalar attribute metadata
|
|
186
188
|
- `GroupMetadata` -- Vector/set/time series group metadata
|
|
187
189
|
- `TimeSeriesData` -- Time series column data
|
|
188
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`
|
|
189
194
|
|
|
190
195
|
## Development
|
|
191
196
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/mod.ts
CHANGED
|
@@ -11,16 +11,4 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export
|
|
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
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 {
|
|
@@ -29,36 +30,28 @@ function setElementArray(
|
|
|
29
30
|
|
|
30
31
|
const first = values[0];
|
|
31
32
|
|
|
32
|
-
// A vector/set read returns a NULL cell as null. The element surface is non-null
|
|
33
|
-
// through
|
|
34
|
-
//
|
|
35
|
-
|
|
33
|
+
// A vector/set/time-series read returns a NULL cell as null. The element surface is non-null
|
|
34
|
+
// (NULL cells go through the group writers), whatever the cell's position or the array's type:
|
|
35
|
+
// the numeric setters below would store a null as 0 / false, and a string array would store an
|
|
36
|
+
// `undefined` as the text "undefined". `includes`, not `some`: it also sees a sparse array's holes.
|
|
37
|
+
if (values.includes(null) || values.includes(undefined)) {
|
|
36
38
|
throw new QuiverError(
|
|
37
|
-
`Unsupported null cell in array '${name}': write NULL cells with updateVectorGroup or
|
|
39
|
+
`Unsupported null cell in array '${name}': write NULL cells with updateVectorGroup, updateSetGroup or updateTimeSeriesGroup`,
|
|
38
40
|
);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
if (typeof first === "bigint") {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (typeof first === "boolean") {
|
|
48
|
-
const arr = allocNativeInt64((values as boolean[]).map((v) => (v ? 1 : 0)));
|
|
49
|
-
check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (typeof first === "number") {
|
|
54
|
-
const allIntegers = (values as number[]).every((v) => Number.isInteger(v));
|
|
55
|
-
if (allIntegers) {
|
|
56
|
-
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);
|
|
57
48
|
check(
|
|
58
49
|
lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null),
|
|
59
50
|
);
|
|
60
51
|
} else {
|
|
61
|
-
|
|
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));
|
|
62
55
|
check(lib.quiver_element_set_array_float(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
|
63
56
|
}
|
|
64
57
|
return;
|
|
@@ -75,7 +68,13 @@ function setElementArray(
|
|
|
75
68
|
throw new QuiverError(`Unsupported array element type for '${name}': ${typeof first}`);
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
function setElementField(
|
|
71
|
+
function setElementField(
|
|
72
|
+
lib: Symbols,
|
|
73
|
+
elemPtr: NativePointer,
|
|
74
|
+
caller: string,
|
|
75
|
+
name: string,
|
|
76
|
+
value: Value,
|
|
77
|
+
): void {
|
|
79
78
|
const nameBuf = toCString(name);
|
|
80
79
|
|
|
81
80
|
if (value === null) {
|
|
@@ -109,7 +108,7 @@ function setElementField(lib: Symbols, elemPtr: NativePointer, name: string, val
|
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
if (Array.isArray(value)) {
|
|
112
|
-
setElementArray(lib, elemPtr, name, value);
|
|
111
|
+
setElementArray(lib, elemPtr, caller, name, value);
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
115
114
|
|
|
@@ -131,7 +130,7 @@ Database.prototype.createElement = function (
|
|
|
131
130
|
try {
|
|
132
131
|
for (const [key, value] of Object.entries(data)) {
|
|
133
132
|
if (value === undefined) continue;
|
|
134
|
-
setElementField(lib, elemPtr, key, value);
|
|
133
|
+
setElementField(lib, elemPtr, "createElement", key, value);
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
const outIdBuf = new Uint8Array(8);
|
|
@@ -159,7 +158,7 @@ Database.prototype.updateElement = function (
|
|
|
159
158
|
try {
|
|
160
159
|
for (const [key, value] of Object.entries(data)) {
|
|
161
160
|
if (value === undefined) continue;
|
|
162
|
-
setElementField(lib, elemPtr, key, value);
|
|
161
|
+
setElementField(lib, elemPtr, "updateElement", key, value);
|
|
163
162
|
}
|
|
164
163
|
const collBuf = toCString(collection);
|
|
165
164
|
check(lib.quiver_database_update_element(handle, collBuf.buf, BigInt(id), elemPtr));
|
|
@@ -185,7 +184,7 @@ Database.prototype.updateElementByLabel = function (
|
|
|
185
184
|
try {
|
|
186
185
|
for (const [key, value] of Object.entries(data)) {
|
|
187
186
|
if (value === undefined) continue;
|
|
188
|
-
setElementField(lib, elemPtr, key, value);
|
|
187
|
+
setElementField(lib, elemPtr, "updateElementByLabel", key, value);
|
|
189
188
|
}
|
|
190
189
|
const collBuf = toCString(collection);
|
|
191
190
|
const labelBuf = toCString(label);
|
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 {
|
|
@@ -186,6 +185,16 @@ export class Database {
|
|
|
186
185
|
attribute: string,
|
|
187
186
|
id: number,
|
|
188
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>[];
|
|
189
198
|
|
|
190
199
|
// --- Queries (implemented in query.ts) ---
|
|
191
200
|
declare queryString: (sql: string, parameters?: QueryParam[]) => string | null;
|
package/src/group-columns.ts
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
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";
|
|
26
|
+
|
|
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)[]>;
|
|
12
32
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Column-oriented group read result: one array of cells per column name, `null` for SQL NULL.
|
|
35
|
+
* The read-side twin of GroupColumns; no reader produces a boolean or a bigint, so it admits
|
|
36
|
+
* neither.
|
|
37
|
+
*/
|
|
38
|
+
export type TimeSeriesData = Record<string, (number | string | null)[]>;
|
|
15
39
|
|
|
16
40
|
/**
|
|
17
41
|
* The parallel-array signature every columnar group update C function shares
|
|
@@ -31,6 +55,34 @@ type ColumnUpdateFn = (
|
|
|
31
55
|
rowCount: bigint,
|
|
32
56
|
) => number;
|
|
33
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
|
+
|
|
34
86
|
/**
|
|
35
87
|
* Marshal a column-oriented payload and forward it to one of the columnar group update C
|
|
36
88
|
* functions. Shared by updateTimeSeriesGroup / updateVectorGroup / updateSetGroup and their
|
|
@@ -94,16 +146,7 @@ export function updateGroupColumns(
|
|
|
94
146
|
const maskPtrs: (Pointer | null)[] = [];
|
|
95
147
|
|
|
96
148
|
for (let c = 0; c < columnCount; c++) {
|
|
97
|
-
const [colName,
|
|
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));
|
|
149
|
+
const [colName, values] = entries[c];
|
|
107
150
|
const first = values.find((v) => v !== null);
|
|
108
151
|
|
|
109
152
|
// Mask via direct indexing — never a DataView, to avoid the documented
|
|
@@ -121,23 +164,29 @@ export function updateGroupColumns(
|
|
|
121
164
|
keepalive.push(p);
|
|
122
165
|
dataPtrs.push(p.ptr);
|
|
123
166
|
} else if (typeof first === "string") {
|
|
167
|
+
// Not checked by numericCells: a mixed ['a', true] column still writes what it always wrote.
|
|
124
168
|
typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
|
|
125
169
|
const { table, keepalive: strPtrs } = allocNativeStringArray(
|
|
126
170
|
values.map((v) => (v === null ? null : (v as string))),
|
|
127
171
|
);
|
|
128
172
|
keepalive.push(table, ...strPtrs);
|
|
129
173
|
dataPtrs.push(table.ptr);
|
|
130
|
-
} else if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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))) {
|
|
134
181
|
typesDv.setInt32(c * 4, DATA_TYPE_INTEGER, true);
|
|
135
|
-
const p = allocNativeInt64(
|
|
182
|
+
const p = allocNativeInt64(cells.map((v) => v ?? 0));
|
|
136
183
|
keepalive.push(p);
|
|
137
184
|
dataPtrs.push(p.ptr);
|
|
138
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.
|
|
139
188
|
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
140
|
-
const p = allocNativeFloat64(
|
|
189
|
+
const p = allocNativeFloat64(cells.map((v) => (v === null ? 0 : Number(v))));
|
|
141
190
|
keepalive.push(p);
|
|
142
191
|
dataPtrs.push(p.ptr);
|
|
143
192
|
}
|
|
@@ -170,3 +219,123 @@ export function updateGroupColumns(
|
|
|
170
219
|
),
|
|
171
220
|
);
|
|
172
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The out-parameter signature every columnar group read C function shares
|
|
225
|
+
* (quiver_database_read_time_series_group, quiver_database_read_{vector,set}_group_by_id).
|
|
226
|
+
*/
|
|
227
|
+
type ColumnReadFn = (
|
|
228
|
+
db: NativePointer,
|
|
229
|
+
collection: Uint8Array,
|
|
230
|
+
group: Uint8Array,
|
|
231
|
+
id: bigint,
|
|
232
|
+
names: Uint8Array,
|
|
233
|
+
types: Uint8Array,
|
|
234
|
+
data: Uint8Array,
|
|
235
|
+
masks: Uint8Array,
|
|
236
|
+
columnCount: Uint8Array,
|
|
237
|
+
rowCount: Uint8Array,
|
|
238
|
+
) => number;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Call one of the columnar group read C functions, decode its typed arrays + per-cell mask into
|
|
242
|
+
* columns, and free the C result. The read-side twin of updateGroupColumns, shared by
|
|
243
|
+
* readTimeSeriesGroup (which returns the columns) and readVectorGroupById / readSetGroupById
|
|
244
|
+
* (which transpose them into rows). A masked cell is `null`; DATE_TIME cells stay ISO 8601 strings.
|
|
245
|
+
*/
|
|
246
|
+
export function readGroupColumns(
|
|
247
|
+
handle: NativePointer,
|
|
248
|
+
readGroup: ColumnReadFn,
|
|
249
|
+
collection: string,
|
|
250
|
+
group: string,
|
|
251
|
+
id: number,
|
|
252
|
+
): TimeSeriesData {
|
|
253
|
+
const collBuf = toCString(collection);
|
|
254
|
+
const grpBuf = toCString(group);
|
|
255
|
+
const outNames = allocPtrOut();
|
|
256
|
+
const outTypes = allocPtrOut();
|
|
257
|
+
const outData = allocPtrOut();
|
|
258
|
+
const outHasValue = allocPtrOut();
|
|
259
|
+
const outColCount = allocUint64Out();
|
|
260
|
+
const outRowCount = allocUint64Out();
|
|
261
|
+
|
|
262
|
+
check(
|
|
263
|
+
readGroup(
|
|
264
|
+
handle,
|
|
265
|
+
collBuf.buf,
|
|
266
|
+
grpBuf.buf,
|
|
267
|
+
BigInt(id),
|
|
268
|
+
outNames.buf,
|
|
269
|
+
outTypes.buf,
|
|
270
|
+
outData.buf,
|
|
271
|
+
outHasValue.buf,
|
|
272
|
+
outColCount.buf,
|
|
273
|
+
outRowCount.buf,
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const colCount = readUint64Out(outColCount);
|
|
278
|
+
const rowCount = readUint64Out(outRowCount);
|
|
279
|
+
if (colCount === 0) return {};
|
|
280
|
+
|
|
281
|
+
const namesPtr = readPtrOut(outNames);
|
|
282
|
+
const typesPtr = readPtrOut(outTypes);
|
|
283
|
+
const dataPtr = readPtrOut(outData);
|
|
284
|
+
const hasValuePtr = readPtrOut(outHasValue);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const colNames = decodeStringArray(namesPtr, colCount);
|
|
288
|
+
const typesAb = toArrayBuffer(typesPtr as Pointer, 0, colCount * 4);
|
|
289
|
+
const types = Array.from(new Int32Array(typesAb));
|
|
290
|
+
const dataPtrs = decodePtrArray(dataPtr, colCount);
|
|
291
|
+
const maskPtrs = decodePtrArray(hasValuePtr, colCount);
|
|
292
|
+
|
|
293
|
+
// Per-cell NULL mask: mask[r] === 0 means SQL NULL, surfaced as JS null. A time series'
|
|
294
|
+
// dimension column's mask is always all 1, so it stays dense.
|
|
295
|
+
const result: TimeSeriesData = {};
|
|
296
|
+
for (let c = 0; c < colCount; c++) {
|
|
297
|
+
const colName = colNames[c];
|
|
298
|
+
const maskPtr = maskPtrs[c];
|
|
299
|
+
const mask = maskPtr ? new Uint8Array(toArrayBuffer(maskPtr as Pointer, 0, rowCount)) : null;
|
|
300
|
+
switch (types[c]) {
|
|
301
|
+
case DATA_TYPE_INTEGER: {
|
|
302
|
+
const vals = decodeInt64Array(dataPtrs[c], rowCount);
|
|
303
|
+
result[colName] = mask ? vals.map((v, r) => (mask[r] ? v : null)) : vals;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case DATA_TYPE_FLOAT: {
|
|
307
|
+
const vals = decodeFloat64Array(dataPtrs[c], rowCount);
|
|
308
|
+
result[colName] = mask ? vals.map((v, r) => (mask[r] ? v : null)) : vals;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case DATA_TYPE_STRING:
|
|
312
|
+
case DATA_TYPE_DATE_TIME: {
|
|
313
|
+
// Read pointer-by-pointer, not with decodeStringArray, which turns a NULL char* into "":
|
|
314
|
+
// a masked-out or NULL cell must come back as null.
|
|
315
|
+
const base = dataPtrs[c];
|
|
316
|
+
const col: (string | null)[] = new Array(rowCount);
|
|
317
|
+
for (let r = 0; r < rowCount; r++) {
|
|
318
|
+
if (mask && !mask[r]) {
|
|
319
|
+
col[r] = null;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const strPtr = base ? read.ptr(base as Pointer, r * 8) : 0;
|
|
323
|
+
col[r] = strPtr === 0 ? null : new CString(strPtr as Pointer).toString();
|
|
324
|
+
}
|
|
325
|
+
result[colName] = col;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
} finally {
|
|
332
|
+
getSymbols().quiver_database_free_time_series_data(
|
|
333
|
+
namesPtr,
|
|
334
|
+
typesPtr,
|
|
335
|
+
dataPtr,
|
|
336
|
+
hasValuePtr,
|
|
337
|
+
BigInt(colCount),
|
|
338
|
+
BigInt(rowCount),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
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,
|
|
@@ -25,6 +24,11 @@ export type {
|
|
|
25
24
|
Value,
|
|
26
25
|
} from "./types.ts";
|
|
27
26
|
export {
|
|
27
|
+
DATA_TYPE_DATE_TIME,
|
|
28
|
+
DATA_TYPE_FLOAT,
|
|
29
|
+
DATA_TYPE_INTEGER,
|
|
30
|
+
DATA_TYPE_NULL,
|
|
31
|
+
DATA_TYPE_STRING,
|
|
28
32
|
LOG_LEVEL_DEBUG,
|
|
29
33
|
LOG_LEVEL_ERROR,
|
|
30
34
|
LOG_LEVEL_INFO,
|
package/src/loader.ts
CHANGED
|
@@ -85,17 +85,22 @@ const readSymbols = {
|
|
|
85
85
|
quiver_database_read_set_integers_by_id: { args: [P, BUF, BUF, I64, P, P, P], returns: I32 },
|
|
86
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,
|
package/src/lua-api.ts
CHANGED
|
@@ -399,7 +399,10 @@ Scalar attributes with no value come back as \`nil\`.
|
|
|
399
399
|
|
|
400
400
|
**Group columns keep NULL cells as \`nil\` holes**, so cell *i* of every column of one group is
|
|
401
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.
|
|
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.
|
|
403
406
|
|
|
404
407
|
---
|
|
405
408
|
|
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);
|
|
@@ -44,6 +50,11 @@ function marshalParams(parameters: QueryParam[]): {
|
|
|
44
50
|
const native = allocNativeInt64([p ? 1 : 0]);
|
|
45
51
|
keepalive.push(native);
|
|
46
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);
|
|
47
58
|
} else if (typeof p === "number") {
|
|
48
59
|
if (Number.isInteger(p)) {
|
|
49
60
|
typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
|
|
@@ -66,10 +77,7 @@ function marshalParams(parameters: QueryParam[]): {
|
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
|
|
70
|
-
const values: Allocation = { ptr: ptr(valuesBuf), buf: valuesBuf };
|
|
71
|
-
keepalive.push(types, values);
|
|
72
|
-
return { types, values, _keepalive: keepalive };
|
|
80
|
+
return { types: typesBuf, values: valuesBuf, count: BigInt(n), _keepalive: keepalive };
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
Database.prototype.queryString = function (
|
|
@@ -81,23 +89,19 @@ Database.prototype.queryString = function (
|
|
|
81
89
|
const sqlBuf = toCString(sql);
|
|
82
90
|
const outValue = allocPtrOut();
|
|
83
91
|
const outHasValue = new Uint8Array(4);
|
|
92
|
+
const params = marshalParams(parameters);
|
|
84
93
|
|
|
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
|
-
}
|
|
94
|
+
check(
|
|
95
|
+
lib.quiver_database_query_string(
|
|
96
|
+
this._handle,
|
|
97
|
+
sqlBuf.buf,
|
|
98
|
+
params.types,
|
|
99
|
+
params.values,
|
|
100
|
+
params.count,
|
|
101
|
+
outValue.buf,
|
|
102
|
+
outHasValue,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
101
105
|
|
|
102
106
|
if (new DataView(outHasValue.buffer).getInt32(0, true) === 0) return null;
|
|
103
107
|
const result = decodeStringFromBuf(outValue);
|
|
@@ -114,23 +118,19 @@ Database.prototype.queryInteger = function (
|
|
|
114
118
|
const sqlBuf = toCString(sql);
|
|
115
119
|
const outValue = new Uint8Array(8);
|
|
116
120
|
const outHasValue = new Uint8Array(4);
|
|
121
|
+
const params = marshalParams(parameters);
|
|
117
122
|
|
|
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
|
-
}
|
|
123
|
+
check(
|
|
124
|
+
lib.quiver_database_query_integer(
|
|
125
|
+
this._handle,
|
|
126
|
+
sqlBuf.buf,
|
|
127
|
+
params.types,
|
|
128
|
+
params.values,
|
|
129
|
+
params.count,
|
|
130
|
+
outValue,
|
|
131
|
+
outHasValue,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
134
|
|
|
135
135
|
if (new DataView(outHasValue.buffer).getInt32(0, true) === 0) return null;
|
|
136
136
|
return Number(new DataView(outValue.buffer).getBigInt64(0, true));
|
|
@@ -153,23 +153,19 @@ Database.prototype.queryFloat = function (
|
|
|
153
153
|
const sqlBuf = toCString(sql);
|
|
154
154
|
const outValue = new Uint8Array(8);
|
|
155
155
|
const outHasValue = new Uint8Array(4);
|
|
156
|
+
const params = marshalParams(parameters);
|
|
156
157
|
|
|
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
|
-
}
|
|
158
|
+
check(
|
|
159
|
+
lib.quiver_database_query_float(
|
|
160
|
+
this._handle,
|
|
161
|
+
sqlBuf.buf,
|
|
162
|
+
params.types,
|
|
163
|
+
params.values,
|
|
164
|
+
params.count,
|
|
165
|
+
outValue,
|
|
166
|
+
outHasValue,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
173
169
|
|
|
174
170
|
if (new DataView(outHasValue.buffer).getInt32(0, true) === 0) return null;
|
|
175
171
|
return new DataView(outValue.buffer).getFloat64(0, true);
|
package/src/read.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readUint64Out,
|
|
15
15
|
toCString,
|
|
16
16
|
} from "./ffi-helpers.ts";
|
|
17
|
+
import { readGroupColumns, type TimeSeriesData } from "./group-columns.ts";
|
|
17
18
|
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
18
19
|
|
|
19
20
|
// --- Scalar array reads ---
|
|
@@ -698,3 +699,58 @@ Database.prototype.readSetStringsById = function (
|
|
|
698
699
|
id,
|
|
699
700
|
);
|
|
700
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);
|
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
|
-
|
|
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 = {
|