quiverdb 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
@@ -71,28 +72,36 @@ bun run example.ts
71
72
  - `createElement(collection, data)` -- Create element, returns numeric ID
72
73
  - `updateElement(collection, id, data)` -- Update element by ID
73
74
  - `deleteElement(collection, id)` -- Delete element by ID
75
+ - `updateElementByLabel(collection, label, data)` -- Update element by label
76
+ - `deleteElementByLabel(collection, label)` -- Delete element by label
74
77
 
75
78
  ### Read (bulk)
76
79
 
77
80
  - `readScalarIntegers(collection, attribute)` -- Read all integer scalars
81
+ - `readScalarBooleans(collection, attribute)` -- Read INTEGER-backed boolean scalars
78
82
  - `readScalarFloats(collection, attribute)` -- Read all float scalars
79
83
  - `readScalarStrings(collection, attribute)` -- Read all string scalars
80
84
  - `readVectorIntegers(collection, attribute)` -- Read all integer vectors
85
+ - `readVectorBooleans(collection, attribute)` -- Read INTEGER-backed boolean vectors
81
86
  - `readVectorFloats(collection, attribute)` -- Read all float vectors
82
87
  - `readVectorStrings(collection, attribute)` -- Read all string vectors
83
88
  - `readSetIntegers(collection, attribute)` -- Read all integer sets
89
+ - `readSetBooleans(collection, attribute)` -- Read INTEGER-backed boolean sets
84
90
  - `readSetFloats(collection, attribute)` -- Read all float sets
85
91
  - `readSetStrings(collection, attribute)` -- Read all string sets
86
92
 
87
93
  ### Read (by ID)
88
94
 
89
95
  - `readScalarIntegerById(collection, attribute, id)` -- Read integer or null
96
+ - `readScalarBooleanById(collection, attribute, id)` -- Read INTEGER-backed boolean or null
90
97
  - `readScalarFloatById(collection, attribute, id)` -- Read float or null
91
98
  - `readScalarStringById(collection, attribute, id)` -- Read string or null
92
99
  - `readVectorIntegersById(collection, attribute, id)` -- Read integer vector
100
+ - `readVectorBooleansById(collection, attribute, id)` -- Read INTEGER-backed boolean vector
93
101
  - `readVectorFloatsById(collection, attribute, id)` -- Read float vector
94
102
  - `readVectorStringsById(collection, attribute, id)` -- Read string vector
95
103
  - `readSetIntegersById(collection, attribute, id)` -- Read integer set
104
+ - `readSetBooleansById(collection, attribute, id)` -- Read INTEGER-backed boolean set
96
105
  - `readSetFloatsById(collection, attribute, id)` -- Read float set
97
106
  - `readSetStringsById(collection, attribute, id)` -- Read string set
98
107
 
@@ -121,9 +130,11 @@ bun run example.ts
121
130
 
122
131
  - `queryString(sql, parameters?)` -- Query returning string or null
123
132
  - `queryInteger(sql, parameters?)` -- Query returning integer or null
133
+ - `queryBoolean(sql, parameters?)` -- Query returning an INTEGER-backed boolean or null
124
134
  - `queryFloat(sql, parameters?)` -- Query returning float or null
125
135
 
126
- 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).
127
138
 
128
139
  ### Transaction
129
140
 
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quiverdb",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
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)));
@@ -148,12 +159,98 @@ Database.prototype.updateElement = function (
148
159
  }
149
160
  };
150
161
 
162
+ /** Label-addressed counterpart of updateElement. */
163
+ Database.prototype.updateElementByLabel = function (
164
+ this: Database,
165
+ collection: string,
166
+ label: string,
167
+ data: ElementData,
168
+ ): void {
169
+ const lib = getSymbols();
170
+ const handle = this._handle;
171
+
172
+ const outElem = allocPtrOut();
173
+ check(lib.quiver_element_create(outElem.buf));
174
+ const elemPtr = readPtrOut(outElem);
175
+
176
+ try {
177
+ for (const [key, value] of Object.entries(data)) {
178
+ if (value === undefined) continue;
179
+ setElementField(lib, elemPtr, key, value);
180
+ }
181
+ const collBuf = toCString(collection);
182
+ const labelBuf = toCString(label);
183
+ check(lib.quiver_database_update_element_by_label(handle, collBuf.buf, labelBuf.buf, elemPtr));
184
+ } finally {
185
+ lib.quiver_element_destroy(elemPtr);
186
+ }
187
+ };
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
+
151
236
  Database.prototype.deleteElement = function (this: Database, collection: string, id: number): void {
152
237
  const lib = getSymbols();
153
238
  const collBuf = toCString(collection);
154
239
  check(lib.quiver_database_delete_element(this._handle, collBuf.buf, BigInt(id)));
155
240
  };
156
241
 
242
+ /** Label-addressed counterpart of deleteElement. */
243
+ Database.prototype.deleteElementByLabel = function (
244
+ this: Database,
245
+ collection: string,
246
+ label: string,
247
+ ): void {
248
+ const lib = getSymbols();
249
+ const collBuf = toCString(collection);
250
+ const labelBuf = toCString(label);
251
+ check(lib.quiver_database_delete_element_by_label(this._handle, collBuf.buf, labelBuf.buf));
252
+ };
253
+
157
254
  /**
158
255
  * Replace all of an element's rows in one *named* vector group, from column arrays keyed by name.
159
256
  *
@@ -180,6 +277,25 @@ Database.prototype.updateVectorGroup = function (
180
277
  );
181
278
  };
182
279
 
280
+ /** Label-addressed counterpart of updateVectorGroup. */
281
+ Database.prototype.updateVectorGroupByLabel = function (
282
+ this: Database,
283
+ collection: string,
284
+ group: string,
285
+ label: string,
286
+ data: GroupColumns,
287
+ ): void {
288
+ updateGroupColumns(
289
+ this._handle,
290
+ "updateVectorGroupByLabel",
291
+ getSymbols().quiver_database_update_vector_group_by_label,
292
+ collection,
293
+ group,
294
+ label,
295
+ data,
296
+ );
297
+ };
298
+
183
299
  /** Set-group counterpart of updateVectorGroup. */
184
300
  Database.prototype.updateSetGroup = function (
185
301
  this: Database,
@@ -198,3 +314,22 @@ Database.prototype.updateSetGroup = function (
198
314
  data,
199
315
  );
200
316
  };
317
+
318
+ /** Label-addressed counterpart of updateSetGroup. */
319
+ Database.prototype.updateSetGroupByLabel = function (
320
+ this: Database,
321
+ collection: string,
322
+ group: string,
323
+ label: string,
324
+ data: GroupColumns,
325
+ ): void {
326
+ updateGroupColumns(
327
+ this._handle,
328
+ "updateSetGroupByLabel",
329
+ getSymbols().quiver_database_update_set_group_by_label,
330
+ collection,
331
+ group,
332
+ label,
333
+ data,
334
+ );
335
+ };
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();
@@ -86,10 +97,27 @@ export class Database {
86
97
  // --- Element CRUD (implemented in create.ts) ---
87
98
  declare createElement: (collection: string, data: ElementData) => number;
88
99
  declare updateElement: (collection: string, id: number, data: ElementData) => void;
100
+ declare updateElementByLabel: (collection: string, label: string, data: ElementData) => void;
89
101
  declare deleteElement: (collection: string, id: number) => void;
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;
90
117
 
91
118
  // --- Reads (implemented in read.ts) ---
92
119
  declare readScalarIntegers: (collection: string, attribute: string) => (number | null)[];
120
+ declare readScalarBooleans: (collection: string, attribute: string) => (boolean | null)[];
93
121
  declare readScalarFloats: (collection: string, attribute: string) => (number | null)[];
94
122
  declare readScalarStrings: (collection: string, attribute: string) => (string | null)[];
95
123
  declare readScalarIntegerById: (
@@ -97,6 +125,11 @@ export class Database {
97
125
  attribute: string,
98
126
  id: number,
99
127
  ) => number | null;
128
+ declare readScalarBooleanById: (
129
+ collection: string,
130
+ attribute: string,
131
+ id: number,
132
+ ) => boolean | null;
100
133
  declare readScalarFloatById: (collection: string, attribute: string, id: number) => number | null;
101
134
  declare readScalarStringById: (
102
135
  collection: string,
@@ -106,21 +139,26 @@ export class Database {
106
139
  declare readElementIds: (collection: string) => number[];
107
140
  declare numberOfElements: (collection: string) => number;
108
141
  declare readVectorIntegers: (collection: string, attribute: string) => number[][];
142
+ declare readVectorBooleans: (collection: string, attribute: string) => boolean[][];
109
143
  declare readVectorFloats: (collection: string, attribute: string) => number[][];
110
144
  declare readVectorStrings: (collection: string, attribute: string) => string[][];
111
145
  declare readVectorIntegersById: (collection: string, attribute: string, id: number) => number[];
146
+ declare readVectorBooleansById: (collection: string, attribute: string, id: number) => boolean[];
112
147
  declare readVectorFloatsById: (collection: string, attribute: string, id: number) => number[];
113
148
  declare readVectorStringsById: (collection: string, attribute: string, id: number) => string[];
114
149
  declare readSetIntegers: (collection: string, attribute: string) => number[][];
150
+ declare readSetBooleans: (collection: string, attribute: string) => boolean[][];
115
151
  declare readSetFloats: (collection: string, attribute: string) => number[][];
116
152
  declare readSetStrings: (collection: string, attribute: string) => string[][];
117
153
  declare readSetIntegersById: (collection: string, attribute: string, id: number) => number[];
154
+ declare readSetBooleansById: (collection: string, attribute: string, id: number) => boolean[];
118
155
  declare readSetFloatsById: (collection: string, attribute: string, id: number) => number[];
119
156
  declare readSetStringsById: (collection: string, attribute: string, id: number) => string[];
120
157
 
121
158
  // --- Queries (implemented in query.ts) ---
122
159
  declare queryString: (sql: string, parameters?: QueryParam[]) => string | null;
123
160
  declare queryInteger: (sql: string, parameters?: QueryParam[]) => number | null;
161
+ declare queryBoolean: (sql: string, parameters?: QueryParam[]) => boolean | null;
124
162
  declare queryFloat: (sql: string, parameters?: QueryParam[]) => number | null;
125
163
 
126
164
  // --- Transactions (implemented in transaction.ts) ---
@@ -156,24 +194,48 @@ export class Database {
156
194
  id: number,
157
195
  data: TimeSeriesData,
158
196
  ) => void;
197
+ declare updateTimeSeriesGroupByLabel: (
198
+ collection: string,
199
+ group: string,
200
+ label: string,
201
+ data: TimeSeriesData,
202
+ ) => void;
159
203
  declare updateVectorGroup: (
160
204
  collection: string,
161
205
  group: string,
162
206
  id: number,
163
207
  data: GroupColumns,
164
208
  ) => void;
209
+ declare updateVectorGroupByLabel: (
210
+ collection: string,
211
+ group: string,
212
+ label: string,
213
+ data: GroupColumns,
214
+ ) => void;
165
215
  declare updateSetGroup: (
166
216
  collection: string,
167
217
  group: string,
168
218
  id: number,
169
219
  data: GroupColumns,
170
220
  ) => void;
221
+ declare updateSetGroupByLabel: (
222
+ collection: string,
223
+ group: string,
224
+ label: string,
225
+ data: GroupColumns,
226
+ ) => void;
171
227
  declare upsertTimeSeriesRow: (
172
228
  collection: string,
173
229
  group: string,
174
230
  id: number,
175
231
  row: Record<string, number | bigint | string>,
176
232
  ) => void;
233
+ declare upsertTimeSeriesRowByLabel: (
234
+ collection: string,
235
+ group: string,
236
+ label: string,
237
+ row: Record<string, number | bigint | string>,
238
+ ) => void;
177
239
  declare hasTimeSeriesFiles: (collection: string) => boolean;
178
240
  declare listTimeSeriesFilesColumns: (collection: string) => string[];
179
241
  declare readTimeSeriesFiles: (collection: string) => Record<string, string | null>;
@@ -15,13 +15,14 @@ export type GroupColumns = Record<string, (number | string | null)[]>;
15
15
 
16
16
  /**
17
17
  * The parallel-array signature every columnar group update C function shares
18
- * (quiver_database_update_{time_series,vector,set}_group).
18
+ * (quiver_database_update_{time_series,vector,set}_group and their _by_label forms). The 4th
19
+ * argument addresses the element: an id for the by-id forms, a NUL-terminated label otherwise.
19
20
  */
20
21
  type ColumnUpdateFn = (
21
22
  db: NativePointer,
22
23
  collection: Uint8Array,
23
24
  group: Uint8Array,
24
- id: bigint,
25
+ key: bigint | Uint8Array,
25
26
  names: Uint8Array | null,
26
27
  types: Uint8Array | null,
27
28
  data: Uint8Array | null,
@@ -32,8 +33,8 @@ type ColumnUpdateFn = (
32
33
 
33
34
  /**
34
35
  * Marshal a column-oriented payload and forward it to one of the columnar group update C
35
- * functions. Shared by updateTimeSeriesGroup / updateVectorGroup / updateSetGroup: the three
36
- * differ only in which C entry point they call.
36
+ * functions. Shared by updateTimeSeriesGroup / updateVectorGroup / updateSetGroup and their
37
+ * _by_label counterparts: they differ only in which C entry point they call.
37
38
  *
38
39
  * Pass `{}` (no columns) to clear the group.
39
40
  */
@@ -43,15 +44,16 @@ export function updateGroupColumns(
43
44
  update: ColumnUpdateFn,
44
45
  collection: string,
45
46
  group: string,
46
- id: number,
47
+ key: number | string,
47
48
  data: GroupColumns,
48
49
  ): void {
49
50
  const collBuf = toCString(collection);
50
51
  const grpBuf = toCString(group);
52
+ const keyArg = typeof key === "string" ? toCString(key).buf : BigInt(key);
51
53
  const entries = Object.entries(data);
52
54
 
53
55
  if (entries.length === 0) {
54
- check(update(handle, collBuf.buf, grpBuf.buf, BigInt(id), null, null, null, null, 0n, 0n));
56
+ check(update(handle, collBuf.buf, grpBuf.buf, keyArg, null, null, null, null, 0n, 0n));
55
57
  return;
56
58
  }
57
59
 
@@ -149,7 +151,7 @@ export function updateGroupColumns(
149
151
  handle,
150
152
  collBuf.buf,
151
153
  grpBuf.buf,
152
- BigInt(id),
154
+ keyArg,
153
155
  namesTable.buf,
154
156
  typesAlloc.buf,
155
157
  dataTable.buf,
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 },
@@ -58,7 +59,11 @@ const elementSymbols = {
58
59
  const crudSymbols = {
59
60
  quiver_database_create_element: { args: [P, BUF, P, P], returns: I32 },
60
61
  quiver_database_update_element: { args: [P, BUF, I64, P], returns: I32 },
62
+ quiver_database_update_element_by_label: { args: [P, BUF, BUF, P], returns: I32 },
61
63
  quiver_database_delete_element: { args: [P, BUF, I64], returns: I32 },
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 },
62
67
  } as const;
63
68
 
64
69
  const readSymbols = {
@@ -131,18 +136,34 @@ const timeSeriesSymbols = {
131
136
  args: [P, BUF, BUF, I64, P, P, P, USIZE],
132
137
  returns: I32,
133
138
  },
139
+ quiver_database_upsert_time_series_row_by_label: {
140
+ args: [P, BUF, BUF, BUF, P, P, P, USIZE],
141
+ returns: I32,
142
+ },
134
143
  quiver_database_update_time_series_group: {
135
144
  args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
136
145
  returns: I32,
137
146
  },
147
+ quiver_database_update_time_series_group_by_label: {
148
+ args: [P, BUF, BUF, BUF, P, P, P, P, USIZE, USIZE],
149
+ returns: I32,
150
+ },
138
151
  quiver_database_update_vector_group: {
139
152
  args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
140
153
  returns: I32,
141
154
  },
155
+ quiver_database_update_vector_group_by_label: {
156
+ args: [P, BUF, BUF, BUF, P, P, P, P, USIZE, USIZE],
157
+ returns: I32,
158
+ },
142
159
  quiver_database_update_set_group: {
143
160
  args: [P, BUF, BUF, I64, P, P, P, P, USIZE, USIZE],
144
161
  returns: I32,
145
162
  },
163
+ quiver_database_update_set_group_by_label: {
164
+ args: [P, BUF, BUF, BUF, P, P, P, P, USIZE, USIZE],
165
+ returns: I32,
166
+ },
146
167
  quiver_database_free_time_series_data: { args: [P, P, P, P, USIZE, USIZE], returns: I32 },
147
168
  quiver_database_has_time_series_files: { args: [P, BUF, P], returns: I32 },
148
169
  quiver_database_list_time_series_files_columns: { args: [P, BUF, P, P], returns: I32 },
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, expr:save) are sandboxed
14
- // to the database file's directory; the pure-metadata builders stay under the quiver.* global.
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,7 +51,7 @@ 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
- | \`nil\` | NULL | In query params, file paths, and ts rows. |
54
+ | \`nil\` | NULL | In query params, file paths, ts rows, relations.|
54
55
  | table (1-indexed) | array | Used for vectors/sets and column-oriented data.|
55
56
 
56
57
  **Unsupported types throw.** Passing a boolean, a function, or a nested table where a scalar is
@@ -59,15 +60,21 @@ attributes, time-series rows, and query parameters. A skipped positional query p
59
60
  shift every later parameter and bind NULL to the trailing placeholder, so this is rejected loudly.
60
61
 
61
62
  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.)
63
+ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Python.) The time part
64
+ is optional, so \`"2024-01-15"\` is also valid, and a space may replace the \`T\`. Every field is
65
+ fixed-width and zero-padded. Anything shorter or malformed — \`"2005"\`, \`"2005-01"\`,
66
+ \`"2024-02-31"\`, \`"2024-1-5"\`, \`"2024-01-15T10:30"\` — is **rejected when you write it**, not
67
+ silently stored. The value is stored exactly as written; a date-only value is not padded to
68
+ midnight.
63
69
 
64
70
  ---
65
71
 
66
72
  ## Critical rules
67
73
 
68
74
  - **Type coercion.** An integer is accepted for a REAL column (coerced to real on insert); a float
69
- is rejected for an INTEGER column. Other type mismatches raise a validation error and roll the
70
- whole script back.
75
+ is rejected for an INTEGER column. A string bound to a \`date_*\` column must parse as ISO 8601
76
+ (\`YYYY-MM-DD\`, optionally \`THH:MM:SS\` or \` HH:MM:SS\`). Other type mismatches raise a
77
+ validation error and roll the whole script back.
71
78
  - **Errors abort the script.** Any error thrown by a \`db:\` call stops the script and surfaces as
72
79
  \`Failed to run Lua script: <message>\`. Validation failures roll back whatever the current
73
80
  transaction covered.
@@ -76,16 +83,16 @@ datetime surface — there are no DateTime wrapper helpers, unlike Julia/Dart/Py
76
83
  and \`dofile\`/\`loadfile\` are removed (string-form \`load\` stays available). Integer division is
77
84
  the Lua 5.4 \`//\` operator — a language operator, unrelated to \`math\`.
78
85
  - **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 relative paths against
80
- the directory containing the database file and rejects anything outside it (subdirectories are
81
- fine; \`..\` escapes and outside absolute paths throw \`Cannot <op>: path '...' escapes the
86
+ \`db:open_file\`, \`db:bin_to_csv\`, \`db:csv_to_bin\`, \`db:validate_migrations\`, \`expr:save\`) resolves
87
+ relative paths against the directory containing the database file and rejects anything outside it
88
+ (subdirectories are fine; \`..\` escapes and outside absolute paths throw \`Cannot <op>: path '...' escapes the
82
89
  database directory ...\`). On an in-memory database these operations throw
83
90
  \`Cannot <op>: database is in-memory, file operations are unavailable\`.
84
91
  - **Output.** A script can \`return\` one value and the host receives it as JSON — prefer this over
85
92
  \`print()\` when you need structured data back (\`print()\` still works and is captured). Only the
86
93
  **first** returned value is encoded. Arrays are 1-indexed (iterate with \`ipairs\`); reading a NULL
87
94
  yields \`nil\`, writing \`nil\` stores NULL where NULL is accepted (query params, ts rows, file
88
- columns — but NOT element scalar attributes; see CRUD).
95
+ columns, relation targets — but NOT element scalar attributes; see CRUD).
89
96
 
90
97
  \`\`\`lua
91
98
  return { ids = db:read_element_ids("Collection"), total = 3 }
@@ -134,10 +141,15 @@ db:describe() -- string: whole-DB text report (returns it,
134
141
  db:describe_collection(collection) -- string: one collection's structure (text report)
135
142
  db:summarize_collection(collection)-- string: per-scalar null/non-null counts, low-cardinality
136
143
  -- integer value distributions, per-group sizes
144
+ db:validate_migrations(path) -- validate a migrations dir (up then down) in-memory; no return
137
145
  \`\`\`
138
146
 
139
147
  All three \`describe*\`/\`summarize*\` methods **return** a string — \`print()\` it to see it.
140
148
 
149
+ \`db:validate_migrations(path)\` applies every \`up.sql\` in version order, then every \`down.sql\` in
150
+ reverse, against a throwaway in-memory database — nothing in \`db\` itself is touched. The round trip
151
+ must end with an empty database; leftover tables are named in the error.
152
+
141
153
  ---
142
154
 
143
155
  ## Transactions
@@ -212,7 +224,12 @@ Rules worth knowing:
212
224
  \`\`\`lua
213
225
  local id = db:create_element(collection, element_table) -- returns new integer id
214
226
  db:update_element(collection, id, element_table)
227
+ db:update_element_by_label(collection, label, element_table) -- same update, addressed by label
215
228
  db:delete_element(collection, id)
229
+ db:delete_element_by_label(collection, label) -- same delete, addressed by label
230
+
231
+ db:update_relation(collection_from, collection_to, relation_type, id, target_label)
232
+ db:update_relation_by_label(collection_from, collection_to, relation_type, label, target_label)
216
233
  \`\`\`
217
234
 
218
235
  The element table holds scalar attributes as \`key = value\`, and vector/set attributes as
@@ -238,6 +255,13 @@ Notes:
238
255
  - **\`update_element\` / \`delete_element\` require an existing id.** Targeting an id that does not
239
256
  exist throws \`Element not found: <id> in collection '<collection>'\` (no silent no-op). Use
240
257
  \`read_element_ids\` to get valid ids.
258
+ - **\`update_element_by_label\` / \`delete_element_by_label\` require an existing label**, unique
259
+ *per collection*, not per database — one naming an element of another collection does not
260
+ resolve. A miss throws \`Element not found: label '<label>' in collection '<collection>'\` and
261
+ changes nothing. Passing \`label = "New name"\` in the element table renames the element, after
262
+ which only the new label resolves. Because the label form delegates to the id form, failures
263
+ that validate the *element* (an empty table, a type mismatch) report
264
+ \`Cannot update_element: ...\`.
241
265
  - **Empty arrays are skipped.** An attribute whose value is \`{}\` writes no vector/set (the element
242
266
  type can't be inferred from an empty array), so it is silently dropped.
243
267
  - **No \`nil\` scalar attributes.** In Lua a key set to \`nil\` is dropped from the table, so
@@ -245,7 +269,18 @@ Notes:
245
269
  **throws** (\`...must have at least one scalar attribute\` on create, \`...at least one attribute
246
270
  to update\` on update). To leave a column unchanged, omit the key — you cannot set a scalar to
247
271
  NULL via the element table. (\`nil\` → NULL is only accepted by
248
- \`upsert_time_series_row\` and \`update_time_series_files\`.)
272
+ \`upsert_time_series_row\`, \`update_time_series_files\` and \`update_relation\`.)
273
+ - **\`update_relation\` points one scalar foreign-key relation at another element**, named by the
274
+ target's label. The column is derived from the naming convention —
275
+ \`lowercase(collection_to) .. "_" .. relation_type\`, so
276
+ \`db:update_relation("Child", "Parent", "id", id, "Parent A")\` writes \`Child.parent_id\`. A
277
+ \`nil\` or omitted \`target_label\` clears the relation; anything that is not a string throws
278
+ (\`target_label has unsupported Lua type\`). The derived column must exist and be a
279
+ foreign key to \`collection_to\`, otherwise \`Cannot update_relation: ...\`. The write delegates
280
+ to \`update_element\`, so a missing id reports that method's error;
281
+ \`update_relation_by_label\` takes a label in place of the id, with
282
+ \`update_element_by_label\`'s resolution and miss semantics. A relation living in a vector, set
283
+ or time-series group is a list of targets — use that group's writer instead.
249
284
 
250
285
  ---
251
286
 
@@ -295,6 +330,9 @@ db:update_vector_group("Child", "refs", id, { parent_ref = { 1, 2, 3 } })
295
330
  db:update_set_group("Child", "parents", id, { parent_ref = { 1, 2 } })
296
331
 
297
332
  db:update_vector_group("Child", "refs", id, {}) -- clears the group
333
+
334
+ db:update_vector_group_by_label("Child", "refs", "Child 1", { parent_ref = { 1, 2 } })
335
+ db:update_set_group_by_label("Child", "parents", "Child 1", { parent_ref = { 1, 2 } })
298
336
  \`\`\`
299
337
 
300
338
  Use these instead of routing a group's columns through \`update_element\` whenever a column name is
@@ -311,7 +349,8 @@ Rules:
311
349
  are rejected if passed.
312
350
  - **Foreign-key columns accept a label string** and resolve it to the referenced id, exactly as in
313
351
  \`create_element\` / \`update_element\`.
314
- - The element id must exist, same as \`update_element\` / \`delete_element\`.
352
+ - The element id must exist, same as \`update_element\` / \`delete_element\`; the \`_by_label\` form
353
+ takes a label in its place, with \`update_element_by_label\`'s resolution and miss semantics.
315
354
 
316
355
  ---
317
356
 
@@ -372,6 +411,8 @@ db:update_time_series_group("Items", "data", id, {
372
411
  })
373
412
 
374
413
  db:update_time_series_group("Items", "data", id, {}) -- clears the group
414
+
415
+ db:update_time_series_group_by_label("Items", "data", "Item 1", { date_time = { "2024-01-01T00:00:00" }, value = { 10.5 } })
375
416
  \`\`\`
376
417
 
377
418
  A read-modify-write looks like this:
@@ -401,6 +442,8 @@ column names). Each value of the top-level table must be an **array**, not a sca
401
442
  table {} to clear the group\`) — only a bare \`{}\` clears.
402
443
  - Integer values are accepted for REAL columns (converted on insert). Booleans, functions, and
403
444
  other unsupported Lua types throw \`column '...' has unsupported Lua type\`.
445
+ - The element id must exist; the \`_by_label\` form takes a label in its place, with
446
+ \`update_element_by_label\`'s resolution and miss semantics. Every rule above applies to both.
404
447
 
405
448
  ### Append/upsert a single row (\`upsert_time_series_row\` — ROW-oriented, the one exception)
406
449
 
@@ -412,8 +455,16 @@ db:upsert_time_series_row("Items", "data", id, {
412
455
  date_time = "2024-01-04T00:00:00",
413
456
  value = 40.0,
414
457
  })
458
+
459
+ db:upsert_time_series_row_by_label("Items", "data", "Item 1", {
460
+ date_time = "2024-01-04T00:00:00",
461
+ value = 40.0,
462
+ })
415
463
  \`\`\`
416
464
 
465
+ The element id must exist; the \`_by_label\` form takes a label in its place, with
466
+ \`update_element_by_label\`'s resolution and miss semantics.
467
+
417
468
  ---
418
469
 
419
470
  ## Time series files
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,
@@ -18,7 +18,7 @@ import {
18
18
  toCString,
19
19
  } from "./ffi-helpers.ts";
20
20
  import { updateGroupColumns } from "./group-columns.ts";
21
- import { getSymbols } from "./loader.ts";
21
+ import { getSymbols, type NativePointer } from "./loader.ts";
22
22
  import {
23
23
  type Allocation,
24
24
  DATA_TYPE_DATE_TIME,
@@ -200,16 +200,57 @@ Database.prototype.updateTimeSeriesGroup = function (
200
200
  );
201
201
  };
202
202
 
203
- Database.prototype.upsertTimeSeriesRow = function (
203
+ /** Label-addressed counterpart of updateTimeSeriesGroup. */
204
+ Database.prototype.updateTimeSeriesGroupByLabel = function (
204
205
  this: Database,
205
206
  collection: string,
206
207
  group: string,
207
- id: number,
208
+ label: string,
209
+ data: TimeSeriesData,
210
+ ): void {
211
+ updateGroupColumns(
212
+ this._handle,
213
+ "updateTimeSeriesGroupByLabel",
214
+ getSymbols().quiver_database_update_time_series_group_by_label,
215
+ collection,
216
+ group,
217
+ label,
218
+ data,
219
+ );
220
+ };
221
+
222
+ /**
223
+ * The parallel-array signature both row-oriented time-series upsert C functions share
224
+ * (quiver_database_upsert_time_series_row and its _by_label form). `key` is an id for the by-id
225
+ * form, a NUL-terminated label otherwise.
226
+ */
227
+ type UpsertRowFn = (
228
+ db: NativePointer,
229
+ collection: Uint8Array,
230
+ group: Uint8Array,
231
+ key: bigint | Uint8Array,
232
+ names: Uint8Array | null,
233
+ types: Uint8Array | null,
234
+ data: Uint8Array | null,
235
+ columnCount: bigint,
236
+ ) => number;
237
+
238
+ /**
239
+ * Marshal a single row of scalars and forward it to one of the row-oriented time-series upsert
240
+ * C functions. Shared by upsertTimeSeriesRow / upsertTimeSeriesRowByLabel: they differ only in
241
+ * which C entry point they call and whether `key` is an id or a label.
242
+ */
243
+ function upsertRowColumns(
244
+ handle: NativePointer,
245
+ upsert: UpsertRowFn,
246
+ collection: string,
247
+ group: string,
248
+ key: number | string,
208
249
  row: Record<string, number | bigint | string>,
209
250
  ): void {
210
- const lib = getSymbols();
211
251
  const collBuf = toCString(collection);
212
252
  const grpBuf = toCString(group);
253
+ const keyArg = typeof key === "string" ? toCString(key).buf : BigInt(key);
213
254
  const entries = Object.entries(row);
214
255
  const columnCount = entries.length;
215
256
  const keepalive: Allocation[] = [];
@@ -248,17 +289,52 @@ Database.prototype.upsertTimeSeriesRow = function (
248
289
  keepalive.push(dataTable);
249
290
 
250
291
  check(
251
- lib.quiver_database_upsert_time_series_row(
252
- this._handle,
292
+ upsert(
293
+ handle,
253
294
  collBuf.buf,
254
295
  grpBuf.buf,
255
- BigInt(id),
296
+ keyArg,
256
297
  namesTable.buf,
257
298
  typesAlloc.buf,
258
299
  dataTable.buf,
259
300
  BigInt(columnCount),
260
301
  ),
261
302
  );
303
+ }
304
+
305
+ Database.prototype.upsertTimeSeriesRow = function (
306
+ this: Database,
307
+ collection: string,
308
+ group: string,
309
+ id: number,
310
+ row: Record<string, number | bigint | string>,
311
+ ): void {
312
+ upsertRowColumns(
313
+ this._handle,
314
+ getSymbols().quiver_database_upsert_time_series_row,
315
+ collection,
316
+ group,
317
+ id,
318
+ row,
319
+ );
320
+ };
321
+
322
+ /** Label-addressed counterpart of upsertTimeSeriesRow. */
323
+ Database.prototype.upsertTimeSeriesRowByLabel = function (
324
+ this: Database,
325
+ collection: string,
326
+ group: string,
327
+ label: string,
328
+ row: Record<string, number | bigint | string>,
329
+ ): void {
330
+ upsertRowColumns(
331
+ this._handle,
332
+ getSymbols().quiver_database_upsert_time_series_row_by_label,
333
+ collection,
334
+ group,
335
+ label,
336
+ row,
337
+ );
262
338
  };
263
339
 
264
340
  Database.prototype.hasTimeSeriesFiles = function (this: Database, collection: string): boolean {
package/src/types.ts CHANGED
@@ -21,11 +21,12 @@ export type DatabaseOptions = {
21
21
  consoleLevel?: number;
22
22
  };
23
23
 
24
- export type ScalarValue = number | bigint | string | null;
25
- export type ArrayValue = number[] | bigint[] | string[];
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 = {