quiverdb 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- 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 -0
- package/package.json +1 -1
- package/src/boolean.ts +20 -0
- package/src/create.ts +58 -0
- package/src/database.ts +40 -4
- package/src/group-columns.ts +11 -2
- package/src/loader.ts +3 -0
- package/src/lua-api.ts +66 -19
- package/src/query.ts +14 -0
- package/src/read.ts +67 -0
- package/src/time-series.ts +22 -9
- package/src/types.ts +4 -3
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ bun run example.ts
|
|
|
64
64
|
|
|
65
65
|
- `Database.fromSchema(dbPath, schemaPath)` -- Create database from SQL schema file
|
|
66
66
|
- `Database.fromMigrations(dbPath, migrationsPath)` -- Create database from migrations directory
|
|
67
|
+
- `Database.validateMigrations(migrationsPath)` -- Validate a migrations directory (every `up.sql`, then every `down.sql`, ending with no table left behind) in-memory; throws on failure
|
|
67
68
|
- `close()` -- Close the database connection
|
|
68
69
|
|
|
69
70
|
### Create / Delete
|
|
@@ -77,24 +78,30 @@ bun run example.ts
|
|
|
77
78
|
### Read (bulk)
|
|
78
79
|
|
|
79
80
|
- `readScalarIntegers(collection, attribute)` -- Read all integer scalars
|
|
81
|
+
- `readScalarBooleans(collection, attribute)` -- Read INTEGER-backed boolean scalars
|
|
80
82
|
- `readScalarFloats(collection, attribute)` -- Read all float scalars
|
|
81
83
|
- `readScalarStrings(collection, attribute)` -- Read all string scalars
|
|
82
84
|
- `readVectorIntegers(collection, attribute)` -- Read all integer vectors
|
|
85
|
+
- `readVectorBooleans(collection, attribute)` -- Read INTEGER-backed boolean vectors
|
|
83
86
|
- `readVectorFloats(collection, attribute)` -- Read all float vectors
|
|
84
87
|
- `readVectorStrings(collection, attribute)` -- Read all string vectors
|
|
85
88
|
- `readSetIntegers(collection, attribute)` -- Read all integer sets
|
|
89
|
+
- `readSetBooleans(collection, attribute)` -- Read INTEGER-backed boolean sets
|
|
86
90
|
- `readSetFloats(collection, attribute)` -- Read all float sets
|
|
87
91
|
- `readSetStrings(collection, attribute)` -- Read all string sets
|
|
88
92
|
|
|
89
93
|
### Read (by ID)
|
|
90
94
|
|
|
91
95
|
- `readScalarIntegerById(collection, attribute, id)` -- Read integer or null
|
|
96
|
+
- `readScalarBooleanById(collection, attribute, id)` -- Read INTEGER-backed boolean or null
|
|
92
97
|
- `readScalarFloatById(collection, attribute, id)` -- Read float or null
|
|
93
98
|
- `readScalarStringById(collection, attribute, id)` -- Read string or null
|
|
94
99
|
- `readVectorIntegersById(collection, attribute, id)` -- Read integer vector
|
|
100
|
+
- `readVectorBooleansById(collection, attribute, id)` -- Read INTEGER-backed boolean vector
|
|
95
101
|
- `readVectorFloatsById(collection, attribute, id)` -- Read float vector
|
|
96
102
|
- `readVectorStringsById(collection, attribute, id)` -- Read string vector
|
|
97
103
|
- `readSetIntegersById(collection, attribute, id)` -- Read integer set
|
|
104
|
+
- `readSetBooleansById(collection, attribute, id)` -- Read INTEGER-backed boolean set
|
|
98
105
|
- `readSetFloatsById(collection, attribute, id)` -- Read float set
|
|
99
106
|
- `readSetStringsById(collection, attribute, id)` -- Read string set
|
|
100
107
|
|
|
@@ -123,9 +130,11 @@ bun run example.ts
|
|
|
123
130
|
|
|
124
131
|
- `queryString(sql, parameters?)` -- Query returning string or null
|
|
125
132
|
- `queryInteger(sql, parameters?)` -- Query returning integer or null
|
|
133
|
+
- `queryBoolean(sql, parameters?)` -- Query returning an INTEGER-backed boolean or null
|
|
126
134
|
- `queryFloat(sql, parameters?)` -- Query returning float or null
|
|
127
135
|
|
|
128
|
-
Parameters are passed as an array of `number | string | null
|
|
136
|
+
Parameters are passed as an array of `number | boolean | string | null` (a `boolean` binds as the
|
|
137
|
+
INTEGER 1 or 0).
|
|
129
138
|
|
|
130
139
|
### Transaction
|
|
131
140
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/mod.ts
CHANGED
package/package.json
CHANGED
package/src/boolean.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function integerToBoolean(value: number, collection?: string, attribute?: string): boolean;
|
|
2
|
+
export function integerToBoolean(value: null, collection?: string, attribute?: string): null;
|
|
3
|
+
export function integerToBoolean(
|
|
4
|
+
value: number | null,
|
|
5
|
+
collection?: string,
|
|
6
|
+
attribute?: string,
|
|
7
|
+
): boolean | null;
|
|
8
|
+
export function integerToBoolean(
|
|
9
|
+
value: number | null,
|
|
10
|
+
collection?: string,
|
|
11
|
+
attribute?: string,
|
|
12
|
+
): boolean | null {
|
|
13
|
+
if (value === null) return null;
|
|
14
|
+
if (value === 0) return false;
|
|
15
|
+
if (value === 1) return true;
|
|
16
|
+
const source = collection ? ` in '${collection}.${attribute}'` : "";
|
|
17
|
+
// A RangeError, not a QuiverError: the message is crafted here, not read from
|
|
18
|
+
// quiver_get_last_error — these readers are a binding-only convenience.
|
|
19
|
+
throw new RangeError(`Cannot convert integer ${value} to boolean${source}: expected 0 or 1`);
|
|
20
|
+
}
|
package/src/create.ts
CHANGED
|
@@ -35,6 +35,12 @@ function setElementArray(
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
if (typeof first === "boolean") {
|
|
39
|
+
const arr = allocNativeInt64((values as boolean[]).map((v) => (v ? 1 : 0)));
|
|
40
|
+
check(lib.quiver_element_set_array_integer(elemPtr, nameBuf.buf, arr.buf, values.length, null));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
if (typeof first === "number") {
|
|
39
45
|
const allIntegers = (values as number[]).every((v) => Number.isInteger(v));
|
|
40
46
|
if (allIntegers) {
|
|
@@ -73,6 +79,11 @@ function setElementField(lib: Symbols, elemPtr: NativePointer, name: string, val
|
|
|
73
79
|
return;
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
if (typeof value === "boolean") {
|
|
83
|
+
check(lib.quiver_element_set_integer(elemPtr, nameBuf.buf, value ? 1n : 0n));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
if (typeof value === "number") {
|
|
77
88
|
if (Number.isInteger(value)) {
|
|
78
89
|
check(lib.quiver_element_set_integer(elemPtr, nameBuf.buf, BigInt(value)));
|
|
@@ -175,6 +186,53 @@ Database.prototype.updateElementByLabel = function (
|
|
|
175
186
|
}
|
|
176
187
|
};
|
|
177
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Points one scalar foreign-key relation at the element labeled `targetLabel`; `null` clears it.
|
|
191
|
+
* The column is derived as `collectionTo.toLowerCase() + "_" + relationType`.
|
|
192
|
+
*/
|
|
193
|
+
Database.prototype.updateRelation = function (
|
|
194
|
+
this: Database,
|
|
195
|
+
collectionFrom: string,
|
|
196
|
+
collectionTo: string,
|
|
197
|
+
relationType: string,
|
|
198
|
+
id: number,
|
|
199
|
+
targetLabel: string | null,
|
|
200
|
+
): void {
|
|
201
|
+
const lib = getSymbols();
|
|
202
|
+
check(
|
|
203
|
+
lib.quiver_database_update_relation(
|
|
204
|
+
this._handle,
|
|
205
|
+
toCString(collectionFrom).buf,
|
|
206
|
+
toCString(collectionTo).buf,
|
|
207
|
+
toCString(relationType).buf,
|
|
208
|
+
BigInt(id),
|
|
209
|
+
targetLabel === null ? null : toCString(targetLabel).buf,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Label-addressed counterpart of updateRelation. */
|
|
215
|
+
Database.prototype.updateRelationByLabel = function (
|
|
216
|
+
this: Database,
|
|
217
|
+
collectionFrom: string,
|
|
218
|
+
collectionTo: string,
|
|
219
|
+
relationType: string,
|
|
220
|
+
label: string,
|
|
221
|
+
targetLabel: string | null,
|
|
222
|
+
): void {
|
|
223
|
+
const lib = getSymbols();
|
|
224
|
+
check(
|
|
225
|
+
lib.quiver_database_update_relation_by_label(
|
|
226
|
+
this._handle,
|
|
227
|
+
toCString(collectionFrom).buf,
|
|
228
|
+
toCString(collectionTo).buf,
|
|
229
|
+
toCString(relationType).buf,
|
|
230
|
+
toCString(label).buf,
|
|
231
|
+
targetLabel === null ? null : toCString(targetLabel).buf,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
178
236
|
Database.prototype.deleteElement = function (this: Database, collection: string, id: number): void {
|
|
179
237
|
const lib = getSymbols();
|
|
180
238
|
const collBuf = toCString(collection);
|
package/src/database.ts
CHANGED
|
@@ -64,6 +64,17 @@ export class Database {
|
|
|
64
64
|
return new Database(readPtrOut(outDb));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Applies every migration's up.sql in order, then every down.sql in reverse.
|
|
69
|
+
* Throws on failure, including a round trip that leaves any table behind.
|
|
70
|
+
*/
|
|
71
|
+
static validateMigrations(migrationsPath: string): void {
|
|
72
|
+
const lib = getSymbols();
|
|
73
|
+
const migrPathBuf = toCString(migrationsPath);
|
|
74
|
+
|
|
75
|
+
check(lib.quiver_database_validate_migrations(migrPathBuf.buf));
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
close(): void {
|
|
68
79
|
if (this._closed) return;
|
|
69
80
|
const lib = getSymbols();
|
|
@@ -89,9 +100,24 @@ export class Database {
|
|
|
89
100
|
declare updateElementByLabel: (collection: string, label: string, data: ElementData) => void;
|
|
90
101
|
declare deleteElement: (collection: string, id: number) => void;
|
|
91
102
|
declare deleteElementByLabel: (collection: string, label: string) => void;
|
|
103
|
+
declare updateRelation: (
|
|
104
|
+
collectionFrom: string,
|
|
105
|
+
collectionTo: string,
|
|
106
|
+
relationType: string,
|
|
107
|
+
id: number,
|
|
108
|
+
targetLabel: string | null,
|
|
109
|
+
) => void;
|
|
110
|
+
declare updateRelationByLabel: (
|
|
111
|
+
collectionFrom: string,
|
|
112
|
+
collectionTo: string,
|
|
113
|
+
relationType: string,
|
|
114
|
+
label: string,
|
|
115
|
+
targetLabel: string | null,
|
|
116
|
+
) => void;
|
|
92
117
|
|
|
93
118
|
// --- Reads (implemented in read.ts) ---
|
|
94
119
|
declare readScalarIntegers: (collection: string, attribute: string) => (number | null)[];
|
|
120
|
+
declare readScalarBooleans: (collection: string, attribute: string) => (boolean | null)[];
|
|
95
121
|
declare readScalarFloats: (collection: string, attribute: string) => (number | null)[];
|
|
96
122
|
declare readScalarStrings: (collection: string, attribute: string) => (string | null)[];
|
|
97
123
|
declare readScalarIntegerById: (
|
|
@@ -99,6 +125,11 @@ export class Database {
|
|
|
99
125
|
attribute: string,
|
|
100
126
|
id: number,
|
|
101
127
|
) => number | null;
|
|
128
|
+
declare readScalarBooleanById: (
|
|
129
|
+
collection: string,
|
|
130
|
+
attribute: string,
|
|
131
|
+
id: number,
|
|
132
|
+
) => boolean | null;
|
|
102
133
|
declare readScalarFloatById: (collection: string, attribute: string, id: number) => number | null;
|
|
103
134
|
declare readScalarStringById: (
|
|
104
135
|
collection: string,
|
|
@@ -108,21 +139,26 @@ export class Database {
|
|
|
108
139
|
declare readElementIds: (collection: string) => number[];
|
|
109
140
|
declare numberOfElements: (collection: string) => number;
|
|
110
141
|
declare readVectorIntegers: (collection: string, attribute: string) => number[][];
|
|
142
|
+
declare readVectorBooleans: (collection: string, attribute: string) => boolean[][];
|
|
111
143
|
declare readVectorFloats: (collection: string, attribute: string) => number[][];
|
|
112
144
|
declare readVectorStrings: (collection: string, attribute: string) => string[][];
|
|
113
145
|
declare readVectorIntegersById: (collection: string, attribute: string, id: number) => number[];
|
|
146
|
+
declare readVectorBooleansById: (collection: string, attribute: string, id: number) => boolean[];
|
|
114
147
|
declare readVectorFloatsById: (collection: string, attribute: string, id: number) => number[];
|
|
115
148
|
declare readVectorStringsById: (collection: string, attribute: string, id: number) => string[];
|
|
116
149
|
declare readSetIntegers: (collection: string, attribute: string) => number[][];
|
|
150
|
+
declare readSetBooleans: (collection: string, attribute: string) => boolean[][];
|
|
117
151
|
declare readSetFloats: (collection: string, attribute: string) => number[][];
|
|
118
152
|
declare readSetStrings: (collection: string, attribute: string) => string[][];
|
|
119
153
|
declare readSetIntegersById: (collection: string, attribute: string, id: number) => number[];
|
|
154
|
+
declare readSetBooleansById: (collection: string, attribute: string, id: number) => boolean[];
|
|
120
155
|
declare readSetFloatsById: (collection: string, attribute: string, id: number) => number[];
|
|
121
156
|
declare readSetStringsById: (collection: string, attribute: string, id: number) => string[];
|
|
122
157
|
|
|
123
158
|
// --- Queries (implemented in query.ts) ---
|
|
124
159
|
declare queryString: (sql: string, parameters?: QueryParam[]) => string | null;
|
|
125
160
|
declare queryInteger: (sql: string, parameters?: QueryParam[]) => number | null;
|
|
161
|
+
declare queryBoolean: (sql: string, parameters?: QueryParam[]) => boolean | null;
|
|
126
162
|
declare queryFloat: (sql: string, parameters?: QueryParam[]) => number | null;
|
|
127
163
|
|
|
128
164
|
// --- Transactions (implemented in transaction.ts) ---
|
|
@@ -156,13 +192,13 @@ export class Database {
|
|
|
156
192
|
collection: string,
|
|
157
193
|
group: string,
|
|
158
194
|
id: number,
|
|
159
|
-
data:
|
|
195
|
+
data: GroupColumns,
|
|
160
196
|
) => void;
|
|
161
197
|
declare updateTimeSeriesGroupByLabel: (
|
|
162
198
|
collection: string,
|
|
163
199
|
group: string,
|
|
164
200
|
label: string,
|
|
165
|
-
data:
|
|
201
|
+
data: GroupColumns,
|
|
166
202
|
) => void;
|
|
167
203
|
declare updateVectorGroup: (
|
|
168
204
|
collection: string,
|
|
@@ -192,13 +228,13 @@ export class Database {
|
|
|
192
228
|
collection: string,
|
|
193
229
|
group: string,
|
|
194
230
|
id: number,
|
|
195
|
-
row: Record<string, number | bigint | string>,
|
|
231
|
+
row: Record<string, number | bigint | string | boolean>,
|
|
196
232
|
) => void;
|
|
197
233
|
declare upsertTimeSeriesRowByLabel: (
|
|
198
234
|
collection: string,
|
|
199
235
|
group: string,
|
|
200
236
|
label: string,
|
|
201
|
-
row: Record<string, number | bigint | string>,
|
|
237
|
+
row: Record<string, number | bigint | string | boolean>,
|
|
202
238
|
) => void;
|
|
203
239
|
declare hasTimeSeriesFiles: (collection: string) => boolean;
|
|
204
240
|
declare listTimeSeriesFilesColumns: (collection: string) => string[];
|
package/src/group-columns.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { NativePointer } from "./loader.ts";
|
|
|
11
11
|
import { DATA_TYPE_FLOAT, DATA_TYPE_INTEGER, DATA_TYPE_STRING, type Allocation } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
/** Column-oriented group payload: one array of cells per column name, `null` for SQL NULL. */
|
|
14
|
-
export type GroupColumns = Record<string, (number | string | null)[]>;
|
|
14
|
+
export type GroupColumns = Record<string, (number | string | boolean | null)[]>;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The parallel-array signature every columnar group update C function shares
|
|
@@ -94,7 +94,16 @@ export function updateGroupColumns(
|
|
|
94
94
|
const maskPtrs: (Pointer | null)[] = [];
|
|
95
95
|
|
|
96
96
|
for (let c = 0; c < columnCount; c++) {
|
|
97
|
-
const [colName,
|
|
97
|
+
const [colName, rawValues] = entries[c];
|
|
98
|
+
// SQLite has no boolean type: a boolean is INTEGER 1/0, as in setElementField and
|
|
99
|
+
// marshalParams. Normalizing per cell before the dispatch (rather than adding a boolean
|
|
100
|
+
// branch after it) is what makes a mixed [true, 5] column write 1 and 5 instead of
|
|
101
|
+
// truthiness-mapping every cell, and matches Python's per-cell `int(v)`. A string column is
|
|
102
|
+
// left alone so normalizing cannot change what a mixed ['a', true] column already wrote.
|
|
103
|
+
const isStringColumn = typeof rawValues.find((v) => v !== null) === "string";
|
|
104
|
+
const values = isStringColumn
|
|
105
|
+
? rawValues
|
|
106
|
+
: rawValues.map((v) => (typeof v === "boolean" ? (v ? 1 : 0) : v));
|
|
98
107
|
const first = values.find((v) => v !== null);
|
|
99
108
|
|
|
100
109
|
// Mask via direct indexing — never a DataView, to avoid the documented
|
package/src/loader.ts
CHANGED
|
@@ -35,6 +35,7 @@ const lifecycleSymbols = {
|
|
|
35
35
|
// struct in JS.
|
|
36
36
|
quiver_database_from_schema: { args: [BUF, BUF, BUF, P], returns: I32 },
|
|
37
37
|
quiver_database_from_migrations: { args: [BUF, BUF, BUF, P], returns: I32 },
|
|
38
|
+
quiver_database_validate_migrations: { args: [BUF], returns: I32 },
|
|
38
39
|
quiver_database_open: { args: [BUF, BUF, P], returns: I32 },
|
|
39
40
|
quiver_database_close: { args: [P], returns: I32 },
|
|
40
41
|
quiver_database_is_healthy: { args: [P, P], returns: I32 },
|
|
@@ -61,6 +62,8 @@ const crudSymbols = {
|
|
|
61
62
|
quiver_database_update_element_by_label: { args: [P, BUF, BUF, P], returns: I32 },
|
|
62
63
|
quiver_database_delete_element: { args: [P, BUF, I64], returns: I32 },
|
|
63
64
|
quiver_database_delete_element_by_label: { args: [P, BUF, BUF], returns: I32 },
|
|
65
|
+
quiver_database_update_relation: { args: [P, BUF, BUF, BUF, I64, BUF], returns: I32 },
|
|
66
|
+
quiver_database_update_relation_by_label: { args: [P, BUF, BUF, BUF, BUF, BUF], returns: I32 },
|
|
64
67
|
} as const;
|
|
65
68
|
|
|
66
69
|
const readSymbols = {
|
package/src/lua-api.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// and whether the prose is semantically true.
|
|
11
11
|
//
|
|
12
12
|
// NOTE: the binary/expression subsystems are bound in the native binding and documented below.
|
|
13
|
-
// File-touching operations (db:open_file, db:bin_to_csv, db:csv_to_bin,
|
|
14
|
-
// to the database file's directory; the pure-metadata builders stay under
|
|
13
|
+
// File-touching operations (db:open_file, db:bin_to_csv, db:csv_to_bin, db:validate_migrations,
|
|
14
|
+
// expr:save) are sandboxed to the database file's directory; the pure-metadata builders stay under
|
|
15
|
+
// the quiver.* global.
|
|
15
16
|
//
|
|
16
17
|
// FORMAT CONVENTION: every db: method appears at least once as the literal token
|
|
17
18
|
// `db:<snake_case_name>`, and every quiver.* function as `quiver.<name>`, so coverage is greppable
|
|
@@ -50,24 +51,42 @@ Lua values map to Quiver column values as follows:
|
|
|
50
51
|
| integer | INTEGER | Also accepted for REAL columns (coerced to real). |
|
|
51
52
|
| number (float) | REAL | A float is rejected for an INTEGER column. |
|
|
52
53
|
| string | TEXT | Also used for \`date_time\` columns (ISO 8601). |
|
|
53
|
-
|
|
|
54
|
+
| boolean | INTEGER 1/0 | SQLite has no boolean type; \`true\` writes 1, \`false\` 0. |
|
|
55
|
+
| \`nil\` | NULL | In query params, file paths, ts rows, relations.|
|
|
54
56
|
| table (1-indexed) | array | Used for vectors/sets and column-oriented data.|
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
A boolean is accepted **wherever an integer is** — element scalars and arrays, query parameters,
|
|
59
|
+
the group writers, and \`upsert_time_series_row\` — so it also reaches a REAL column through the
|
|
60
|
+
usual int-for-REAL coercion. Reading is the asymmetric half: there are no boolean readers in Lua
|
|
61
|
+
(unlike Julia/Dart/Python/JS), so a stored flag comes back as \`0\`/\`1\` from
|
|
62
|
+
\`read_scalar_integers\`. Compare against 0 rather than truth-testing —
|
|
63
|
+
\`read_scalar_integers(c, a)[i] == 1\` — because in Lua a \`nil\` from a NULL cell is *not* equal to
|
|
64
|
+
0 and \`nil ~= 0\` evaluates to \`true\`.
|
|
65
|
+
|
|
66
|
+
The one place a boolean is deliberately refused is \`db:update_relation\`, where the argument is a
|
|
67
|
+
target label and only \`nil\` (or omitting it) may clear the relation.
|
|
68
|
+
|
|
69
|
+
**Unsupported types throw.** Passing a function or a nested table where a scalar is expected raises
|
|
70
|
+
an error rather than silently dropping the value. This applies to element attributes, time-series
|
|
71
|
+
rows, and query parameters. A skipped positional query parameter would shift every later parameter
|
|
72
|
+
and bind NULL to the trailing placeholder, so this is rejected loudly.
|
|
60
73
|
|
|
61
74
|
Dates are plain strings in ISO 8601 format: \`YYYY-MM-DDTHH:MM:SS\`. (Lua keeps a string-based
|
|
62
|
-
datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Python.)
|
|
75
|
+
datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Python.) The time part
|
|
76
|
+
is optional, so \`"2024-01-15"\` is also valid, and a space may replace the \`T\`. Every field is
|
|
77
|
+
fixed-width and zero-padded. Anything shorter or malformed — \`"2005"\`, \`"2005-01"\`,
|
|
78
|
+
\`"2024-02-31"\`, \`"2024-1-5"\`, \`"2024-01-15T10:30"\` — is **rejected when you write it**, not
|
|
79
|
+
silently stored. The value is stored exactly as written; a date-only value is not padded to
|
|
80
|
+
midnight.
|
|
63
81
|
|
|
64
82
|
---
|
|
65
83
|
|
|
66
84
|
## Critical rules
|
|
67
85
|
|
|
68
86
|
- **Type coercion.** An integer is accepted for a REAL column (coerced to real on insert); a float
|
|
69
|
-
is rejected for an INTEGER column.
|
|
70
|
-
|
|
87
|
+
is rejected for an INTEGER column. A string bound to a \`date_*\` column must parse as ISO 8601
|
|
88
|
+
(\`YYYY-MM-DD\`, optionally \`THH:MM:SS\` or \` HH:MM:SS\`). Other type mismatches raise a
|
|
89
|
+
validation error and roll the whole script back.
|
|
71
90
|
- **Errors abort the script.** Any error thrown by a \`db:\` call stops the script and surfaces as
|
|
72
91
|
\`Failed to run Lua script: <message>\`. Validation failures roll back whatever the current
|
|
73
92
|
transaction covered.
|
|
@@ -76,16 +95,16 @@ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Py
|
|
|
76
95
|
and \`dofile\`/\`loadfile\` are removed (string-form \`load\` stays available). Integer division is
|
|
77
96
|
the Lua 5.4 \`//\` operator — a language operator, unrelated to \`math\`.
|
|
78
97
|
- **Filesystem sandbox.** Every file-touching operation (\`db:export_csv\`, \`db:import_csv\`,
|
|
79
|
-
\`db:open_file\`, \`db:bin_to_csv\`, \`db:csv_to_bin\`, \`expr:save\`) resolves
|
|
80
|
-
the directory containing the database file and rejects anything outside it
|
|
81
|
-
fine; \`..\` escapes and outside absolute paths throw \`Cannot <op>: path '...' escapes the
|
|
98
|
+
\`db:open_file\`, \`db:bin_to_csv\`, \`db:csv_to_bin\`, \`db:validate_migrations\`, \`expr:save\`) resolves
|
|
99
|
+
relative paths against the directory containing the database file and rejects anything outside it
|
|
100
|
+
(subdirectories are fine; \`..\` escapes and outside absolute paths throw \`Cannot <op>: path '...' escapes the
|
|
82
101
|
database directory ...\`). On an in-memory database these operations throw
|
|
83
102
|
\`Cannot <op>: database is in-memory, file operations are unavailable\`.
|
|
84
103
|
- **Output.** A script can \`return\` one value and the host receives it as JSON — prefer this over
|
|
85
104
|
\`print()\` when you need structured data back (\`print()\` still works and is captured). Only the
|
|
86
105
|
**first** returned value is encoded. Arrays are 1-indexed (iterate with \`ipairs\`); reading a NULL
|
|
87
106
|
yields \`nil\`, writing \`nil\` stores NULL where NULL is accepted (query params, ts rows, file
|
|
88
|
-
columns — but NOT element scalar attributes; see CRUD).
|
|
107
|
+
columns, relation targets — but NOT element scalar attributes; see CRUD).
|
|
89
108
|
|
|
90
109
|
\`\`\`lua
|
|
91
110
|
return { ids = db:read_element_ids("Collection"), total = 3 }
|
|
@@ -134,10 +153,15 @@ db:describe() -- string: whole-DB text report (returns it,
|
|
|
134
153
|
db:describe_collection(collection) -- string: one collection's structure (text report)
|
|
135
154
|
db:summarize_collection(collection)-- string: per-scalar null/non-null counts, low-cardinality
|
|
136
155
|
-- integer value distributions, per-group sizes
|
|
156
|
+
db:validate_migrations(path) -- validate a migrations dir (up then down) in-memory; no return
|
|
137
157
|
\`\`\`
|
|
138
158
|
|
|
139
159
|
All three \`describe*\`/\`summarize*\` methods **return** a string — \`print()\` it to see it.
|
|
140
160
|
|
|
161
|
+
\`db:validate_migrations(path)\` applies every \`up.sql\` in version order, then every \`down.sql\` in
|
|
162
|
+
reverse, against a throwaway in-memory database — nothing in \`db\` itself is touched. The round trip
|
|
163
|
+
must end with an empty database; leftover tables are named in the error.
|
|
164
|
+
|
|
141
165
|
---
|
|
142
166
|
|
|
143
167
|
## Transactions
|
|
@@ -215,6 +239,9 @@ db:update_element(collection, id, element_table)
|
|
|
215
239
|
db:update_element_by_label(collection, label, element_table) -- same update, addressed by label
|
|
216
240
|
db:delete_element(collection, id)
|
|
217
241
|
db:delete_element_by_label(collection, label) -- same delete, addressed by label
|
|
242
|
+
|
|
243
|
+
db:update_relation(collection_from, collection_to, relation_type, id, target_label)
|
|
244
|
+
db:update_relation_by_label(collection_from, collection_to, relation_type, label, target_label)
|
|
218
245
|
\`\`\`
|
|
219
246
|
|
|
220
247
|
The element table holds scalar attributes as \`key = value\`, and vector/set attributes as
|
|
@@ -254,7 +281,18 @@ Notes:
|
|
|
254
281
|
**throws** (\`...must have at least one scalar attribute\` on create, \`...at least one attribute
|
|
255
282
|
to update\` on update). To leave a column unchanged, omit the key — you cannot set a scalar to
|
|
256
283
|
NULL via the element table. (\`nil\` → NULL is only accepted by
|
|
257
|
-
\`upsert_time_series_row\` and \`
|
|
284
|
+
\`upsert_time_series_row\`, \`update_time_series_files\` and \`update_relation\`.)
|
|
285
|
+
- **\`update_relation\` points one scalar foreign-key relation at another element**, named by the
|
|
286
|
+
target's label. The column is derived from the naming convention —
|
|
287
|
+
\`lowercase(collection_to) .. "_" .. relation_type\`, so
|
|
288
|
+
\`db:update_relation("Child", "Parent", "id", id, "Parent A")\` writes \`Child.parent_id\`. A
|
|
289
|
+
\`nil\` or omitted \`target_label\` clears the relation; anything that is not a string throws
|
|
290
|
+
(\`target_label has unsupported Lua type\`). The derived column must exist and be a
|
|
291
|
+
foreign key to \`collection_to\`, otherwise \`Cannot update_relation: ...\`. The write delegates
|
|
292
|
+
to \`update_element\`, so a missing id reports that method's error;
|
|
293
|
+
\`update_relation_by_label\` takes a label in place of the id, with
|
|
294
|
+
\`update_element_by_label\`'s resolution and miss semantics. A relation living in a vector, set
|
|
295
|
+
or time-series group is a list of targets — use that group's writer instead.
|
|
258
296
|
|
|
259
297
|
---
|
|
260
298
|
|
|
@@ -342,6 +380,13 @@ db:read_element_by_id(collection, id) -- scalars + vectors + sets mer
|
|
|
342
380
|
\`read_element_by_id\` merges every scalar, vector, and set for the element into a single table.
|
|
343
381
|
Scalar attributes with no value come back as \`nil\`.
|
|
344
382
|
|
|
383
|
+
**Group columns are returned densely, with NULL cells dropped.** \`read_vectors_by_id\` and
|
|
384
|
+
\`read_sets_by_id\` read each column independently, and a column read skips its NULL cells — so two
|
|
385
|
+
columns of the same group are **not** positionally aligned with each other whenever one is
|
|
386
|
+
nullable (e.g. an \`ON DELETE SET NULL\` relation). Do not zip them into rows. There is no
|
|
387
|
+
row-aligned group read in Lua; if you need per-row alignment across a nullable group, do that read
|
|
388
|
+
in the host binding instead.
|
|
389
|
+
|
|
345
390
|
---
|
|
346
391
|
|
|
347
392
|
## Time series
|
|
@@ -655,7 +700,9 @@ and \`unit\` default to \`""\`; \`labels\`, \`dimensions\`, \`dimension_sizes\`,
|
|
|
655
700
|
|
|
656
701
|
## What Lua does *not* expose
|
|
657
702
|
|
|
658
|
-
DateTime wrapper helpers (Lua uses ISO 8601 strings)
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
703
|
+
DateTime wrapper helpers (Lua uses ISO 8601 strings), boolean *reader* helpers (a stored flag reads
|
|
704
|
+
back as \`0\`/\`1\` — writing a boolean is supported), \`_by_id\` single-scalar variants (use the
|
|
705
|
+
composite by-id readers or the bulk readers instead), and the row-aligned whole-group readers the
|
|
706
|
+
other bindings have (hence the null-dropping caveat under composite by-id reads). Everything else
|
|
707
|
+
the native binding exposes — CRUD, reads, time series, metadata, query, CSV, and the
|
|
708
|
+
binary/expression subsystems — is documented above and callable.`;
|
package/src/query.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ptr } from "bun:ffi";
|
|
2
|
+
import { integerToBoolean } from "./boolean.ts";
|
|
2
3
|
import { Database } from "./database.ts";
|
|
3
4
|
import { check, QuiverError } from "./errors.ts";
|
|
4
5
|
import {
|
|
@@ -38,6 +39,11 @@ function marshalParams(parameters: QueryParam[]): {
|
|
|
38
39
|
if (p === null) {
|
|
39
40
|
typesDv.setInt32(i * 4, DATA_TYPE_NULL, true);
|
|
40
41
|
valuesDv.setBigInt64(i * 8, 0n, true);
|
|
42
|
+
} else if (typeof p === "boolean") {
|
|
43
|
+
typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
|
|
44
|
+
const native = allocNativeInt64([p ? 1 : 0]);
|
|
45
|
+
keepalive.push(native);
|
|
46
|
+
valuesDv.setBigInt64(i * 8, nativeAddress(native.ptr), true);
|
|
41
47
|
} else if (typeof p === "number") {
|
|
42
48
|
if (Number.isInteger(p)) {
|
|
43
49
|
typesDv.setInt32(i * 4, DATA_TYPE_INTEGER, true);
|
|
@@ -130,6 +136,14 @@ Database.prototype.queryInteger = function (
|
|
|
130
136
|
return Number(new DataView(outValue.buffer).getBigInt64(0, true));
|
|
131
137
|
};
|
|
132
138
|
|
|
139
|
+
Database.prototype.queryBoolean = function (
|
|
140
|
+
this: Database,
|
|
141
|
+
sql: string,
|
|
142
|
+
parameters?: QueryParam[],
|
|
143
|
+
): boolean | null {
|
|
144
|
+
return integerToBoolean(this.queryInteger(sql, parameters));
|
|
145
|
+
};
|
|
146
|
+
|
|
133
147
|
Database.prototype.queryFloat = function (
|
|
134
148
|
this: Database,
|
|
135
149
|
sql: string,
|
package/src/read.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CString, type Pointer, toArrayBuffer } from "bun:ffi";
|
|
2
|
+
import { integerToBoolean } from "./boolean.ts";
|
|
2
3
|
import { Database } from "./database.ts";
|
|
3
4
|
import { check } from "./errors.ts";
|
|
4
5
|
import {
|
|
@@ -52,6 +53,16 @@ Database.prototype.readScalarIntegers = function (
|
|
|
52
53
|
return result;
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
Database.prototype.readScalarBooleans = function (
|
|
57
|
+
this: Database,
|
|
58
|
+
collection: string,
|
|
59
|
+
attribute: string,
|
|
60
|
+
): (boolean | null)[] {
|
|
61
|
+
return this.readScalarIntegers(collection, attribute).map((value) =>
|
|
62
|
+
integerToBoolean(value, collection, attribute),
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
55
66
|
Database.prototype.readScalarFloats = function (
|
|
56
67
|
this: Database,
|
|
57
68
|
collection: string,
|
|
@@ -143,6 +154,19 @@ Database.prototype.readScalarIntegerById = function (
|
|
|
143
154
|
return Number(new DataView(outValBuf.buffer).getBigInt64(0, true));
|
|
144
155
|
};
|
|
145
156
|
|
|
157
|
+
Database.prototype.readScalarBooleanById = function (
|
|
158
|
+
this: Database,
|
|
159
|
+
collection: string,
|
|
160
|
+
attribute: string,
|
|
161
|
+
id: number,
|
|
162
|
+
): boolean | null {
|
|
163
|
+
return integerToBoolean(
|
|
164
|
+
this.readScalarIntegerById(collection, attribute, id),
|
|
165
|
+
collection,
|
|
166
|
+
attribute,
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
146
170
|
Database.prototype.readScalarFloatById = function (
|
|
147
171
|
this: Database,
|
|
148
172
|
collection: string,
|
|
@@ -342,6 +366,19 @@ Database.prototype.readVectorIntegers = function (
|
|
|
342
366
|
attribute,
|
|
343
367
|
);
|
|
344
368
|
};
|
|
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
|
+
*/
|
|
373
|
+
Database.prototype.readVectorBooleans = function (
|
|
374
|
+
this: Database,
|
|
375
|
+
collection: string,
|
|
376
|
+
attribute: string,
|
|
377
|
+
): boolean[][] {
|
|
378
|
+
return this.readVectorIntegers(collection, attribute).map((values) =>
|
|
379
|
+
values.map((value) => integerToBoolean(value, collection, attribute)),
|
|
380
|
+
);
|
|
381
|
+
};
|
|
345
382
|
Database.prototype.readVectorFloats = function (
|
|
346
383
|
this: Database,
|
|
347
384
|
collection: string,
|
|
@@ -381,6 +418,16 @@ Database.prototype.readSetIntegers = function (
|
|
|
381
418
|
attribute,
|
|
382
419
|
);
|
|
383
420
|
};
|
|
421
|
+
/** Same alignment caveat as `readVectorBooleans`: NULL cells dropped, only ids that own rows. */
|
|
422
|
+
Database.prototype.readSetBooleans = function (
|
|
423
|
+
this: Database,
|
|
424
|
+
collection: string,
|
|
425
|
+
attribute: string,
|
|
426
|
+
): boolean[][] {
|
|
427
|
+
return this.readSetIntegers(collection, attribute).map((values) =>
|
|
428
|
+
values.map((value) => integerToBoolean(value, collection, attribute)),
|
|
429
|
+
);
|
|
430
|
+
};
|
|
384
431
|
Database.prototype.readSetFloats = function (
|
|
385
432
|
this: Database,
|
|
386
433
|
collection: string,
|
|
@@ -515,6 +562,16 @@ Database.prototype.readVectorIntegersById = function (
|
|
|
515
562
|
id,
|
|
516
563
|
);
|
|
517
564
|
};
|
|
565
|
+
Database.prototype.readVectorBooleansById = function (
|
|
566
|
+
this: Database,
|
|
567
|
+
collection: string,
|
|
568
|
+
attribute: string,
|
|
569
|
+
id: number,
|
|
570
|
+
): boolean[] {
|
|
571
|
+
return this.readVectorIntegersById(collection, attribute, id).map((value) =>
|
|
572
|
+
integerToBoolean(value, collection, attribute),
|
|
573
|
+
);
|
|
574
|
+
};
|
|
518
575
|
Database.prototype.readVectorFloatsById = function (
|
|
519
576
|
this: Database,
|
|
520
577
|
collection: string,
|
|
@@ -560,6 +617,16 @@ Database.prototype.readSetIntegersById = function (
|
|
|
560
617
|
id,
|
|
561
618
|
);
|
|
562
619
|
};
|
|
620
|
+
Database.prototype.readSetBooleansById = function (
|
|
621
|
+
this: Database,
|
|
622
|
+
collection: string,
|
|
623
|
+
attribute: string,
|
|
624
|
+
id: number,
|
|
625
|
+
): boolean[] {
|
|
626
|
+
return this.readSetIntegersById(collection, attribute, id).map((value) =>
|
|
627
|
+
integerToBoolean(value, collection, attribute),
|
|
628
|
+
);
|
|
629
|
+
};
|
|
563
630
|
Database.prototype.readSetFloatsById = function (
|
|
564
631
|
this: Database,
|
|
565
632
|
collection: string,
|
package/src/time-series.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
readUint64Out,
|
|
18
18
|
toCString,
|
|
19
19
|
} from "./ffi-helpers.ts";
|
|
20
|
-
import { updateGroupColumns } from "./group-columns.ts";
|
|
20
|
+
import { type GroupColumns, updateGroupColumns } from "./group-columns.ts";
|
|
21
21
|
import { getSymbols, type NativePointer } from "./loader.ts";
|
|
22
22
|
import {
|
|
23
23
|
type Allocation,
|
|
@@ -187,7 +187,7 @@ Database.prototype.updateTimeSeriesGroup = function (
|
|
|
187
187
|
collection: string,
|
|
188
188
|
group: string,
|
|
189
189
|
id: number,
|
|
190
|
-
data:
|
|
190
|
+
data: GroupColumns,
|
|
191
191
|
): void {
|
|
192
192
|
updateGroupColumns(
|
|
193
193
|
this._handle,
|
|
@@ -206,7 +206,7 @@ Database.prototype.updateTimeSeriesGroupByLabel = function (
|
|
|
206
206
|
collection: string,
|
|
207
207
|
group: string,
|
|
208
208
|
label: string,
|
|
209
|
-
data:
|
|
209
|
+
data: GroupColumns,
|
|
210
210
|
): void {
|
|
211
211
|
updateGroupColumns(
|
|
212
212
|
this._handle,
|
|
@@ -242,11 +242,12 @@ type UpsertRowFn = (
|
|
|
242
242
|
*/
|
|
243
243
|
function upsertRowColumns(
|
|
244
244
|
handle: NativePointer,
|
|
245
|
+
caller: string,
|
|
245
246
|
upsert: UpsertRowFn,
|
|
246
247
|
collection: string,
|
|
247
248
|
group: string,
|
|
248
249
|
key: number | string,
|
|
249
|
-
row: Record<string, number | bigint | string>,
|
|
250
|
+
row: Record<string, number | bigint | string | boolean>,
|
|
250
251
|
): void {
|
|
251
252
|
const collBuf = toCString(collection);
|
|
252
253
|
const grpBuf = toCString(group);
|
|
@@ -264,7 +265,11 @@ function upsertRowColumns(
|
|
|
264
265
|
const dataPtrs: (Pointer | null)[] = [];
|
|
265
266
|
|
|
266
267
|
for (let c = 0; c < columnCount; c++) {
|
|
267
|
-
const
|
|
268
|
+
const [colName, raw] = entries[c];
|
|
269
|
+
// SQLite has no boolean type: a boolean is INTEGER 1/0. Normalized here rather than tested in
|
|
270
|
+
// the INTEGER condition below, because Number.isInteger(true) is false — a boolean otherwise
|
|
271
|
+
// reaches allocNativeFloat64 and lands in the column as FLOAT 1.0, silently the wrong type.
|
|
272
|
+
const value = typeof raw === "boolean" ? (raw ? 1 : 0) : raw;
|
|
268
273
|
if (typeof value === "string") {
|
|
269
274
|
typesDv.setInt32(c * 4, DATA_TYPE_STRING, true);
|
|
270
275
|
const { table, keepalive: strPtrs } = allocNativeStringArray([value]);
|
|
@@ -275,11 +280,17 @@ function upsertRowColumns(
|
|
|
275
280
|
const p = allocNativeInt64([value]);
|
|
276
281
|
keepalive.push(p);
|
|
277
282
|
dataPtrs.push(p.ptr);
|
|
278
|
-
} else {
|
|
283
|
+
} else if (typeof value === "number") {
|
|
279
284
|
typesDv.setInt32(c * 4, DATA_TYPE_FLOAT, true);
|
|
280
|
-
const p = allocNativeFloat64([value
|
|
285
|
+
const p = allocNativeFloat64([value]);
|
|
281
286
|
keepalive.push(p);
|
|
282
287
|
dataPtrs.push(p.ptr);
|
|
288
|
+
} else {
|
|
289
|
+
// Mirrors updateGroupColumns: never let null/undefined/an object fall through to the FLOAT
|
|
290
|
+
// branch, where Number(null) is 0 and anything else is NaN — both written with no error.
|
|
291
|
+
throw new QuiverError(
|
|
292
|
+
`Cannot ${caller}: column '${colName}' has unsupported value type ${typeof value}`,
|
|
293
|
+
);
|
|
283
294
|
}
|
|
284
295
|
}
|
|
285
296
|
|
|
@@ -307,10 +318,11 @@ Database.prototype.upsertTimeSeriesRow = function (
|
|
|
307
318
|
collection: string,
|
|
308
319
|
group: string,
|
|
309
320
|
id: number,
|
|
310
|
-
row: Record<string, number | bigint | string>,
|
|
321
|
+
row: Record<string, number | bigint | string | boolean>,
|
|
311
322
|
): void {
|
|
312
323
|
upsertRowColumns(
|
|
313
324
|
this._handle,
|
|
325
|
+
"upsertTimeSeriesRow",
|
|
314
326
|
getSymbols().quiver_database_upsert_time_series_row,
|
|
315
327
|
collection,
|
|
316
328
|
group,
|
|
@@ -325,10 +337,11 @@ Database.prototype.upsertTimeSeriesRowByLabel = function (
|
|
|
325
337
|
collection: string,
|
|
326
338
|
group: string,
|
|
327
339
|
label: string,
|
|
328
|
-
row: Record<string, number | bigint | string>,
|
|
340
|
+
row: Record<string, number | bigint | string | boolean>,
|
|
329
341
|
): void {
|
|
330
342
|
upsertRowColumns(
|
|
331
343
|
this._handle,
|
|
344
|
+
"upsertTimeSeriesRowByLabel",
|
|
332
345
|
getSymbols().quiver_database_upsert_time_series_row_by_label,
|
|
333
346
|
collection,
|
|
334
347
|
group,
|
package/src/types.ts
CHANGED
|
@@ -21,11 +21,12 @@ export type DatabaseOptions = {
|
|
|
21
21
|
consoleLevel?: number;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
export type
|
|
24
|
+
/** A `boolean` is stored as the INTEGER 1 or 0 (see `readScalarBooleans` for the read side). */
|
|
25
|
+
export type ScalarValue = number | bigint | boolean | string | null;
|
|
26
|
+
export type ArrayValue = number[] | bigint[] | boolean[] | string[];
|
|
26
27
|
export type Value = ScalarValue | ArrayValue;
|
|
27
28
|
export type ElementData = Record<string, Value | undefined>;
|
|
28
|
-
export type QueryParam = number | string | null;
|
|
29
|
+
export type QueryParam = number | boolean | string | null;
|
|
29
30
|
|
|
30
31
|
/** Native memory allocation result. Callers MUST hold `buf` in scope to prevent GC. */
|
|
31
32
|
export type Allocation = {
|