@romedb/node 0.0.0-stage → 0.2.13
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/LICENSE.md +20 -0
- package/README.md +1021 -2
- package/RUST-LIBRARY-NOTICES.html +7328 -0
- package/THIRD_PARTY_CRATE_NOTICES.md +16853 -0
- package/THIRD_PARTY_NOTICES.md +1077 -0
- package/client-internal.cjs +2 -0
- package/client.cjs +483 -0
- package/client.d.ts +93 -0
- package/client.mjs +496 -0
- package/embedded-client.cjs +238 -0
- package/embedded-client.d.ts +29 -0
- package/embedded-worker.cjs +115 -0
- package/index.cjs +481 -0
- package/index.d.ts +112 -0
- package/native.cjs +20 -0
- package/package.json +68 -3
- package/request-journal.cjs +47 -0
- package/romedb.node +4 -0
- package/values.cjs +211 -0
- package/values.d.ts +15 -0
- package/values.mjs +220 -0
- package/worker.cjs +37 -0
package/index.cjs
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { NativeDatabase, interruptConnection, createCancellationToken, cancelOperation, releaseCancellationToken, vectorFromComponents, vectorFromSparseEntries } = require('./native.cjs');
|
|
3
|
+
class Record {
|
|
4
|
+
constructor(table, key) {
|
|
5
|
+
if (typeof table !== 'string' || !['string','bigint'].includes(typeof key)) throw new TypeError('Record requires a table and string or bigint key');
|
|
6
|
+
this.table = table; this.key = key;
|
|
7
|
+
Object.freeze(this);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
class Vector {
|
|
11
|
+
static float32(values) { return constructVector('float32', values); }
|
|
12
|
+
static float64(values) { return constructVector('float64', values); }
|
|
13
|
+
static sparse32(values) { return constructVector('sparse32', values); }
|
|
14
|
+
static sparse32Entries(dimensions, entries) {
|
|
15
|
+
if (!Number.isInteger(dimensions) || dimensions < 1 || dimensions > 65536) throw new RangeError('vector dimensions must be 1..65536');
|
|
16
|
+
if (!Array.isArray(entries)) throw new TypeError('sparse entries require an array of index/value pairs');
|
|
17
|
+
if (entries.length > dimensions) throw new RangeError('sparse entry count exceeds dimensions');
|
|
18
|
+
const bytes = Buffer.alloc(entries.length * 12);
|
|
19
|
+
let previous = -1;
|
|
20
|
+
for (let i = 0; i < entries.length; i++) {
|
|
21
|
+
const entry = entries[i];
|
|
22
|
+
if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError('sparse entries require index/value pairs');
|
|
23
|
+
const [index, value] = entry;
|
|
24
|
+
if (!Number.isInteger(index) || index <= previous || index >= dimensions) throw new RangeError('sparse indices must be increasing and within dimensions');
|
|
25
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) throw new TypeError('vector components must be finite numbers');
|
|
26
|
+
if (!Number.isFinite(Math.fround(value))) throw new RangeError('vector component is outside the finite float32 range');
|
|
27
|
+
bytes.writeUInt32LE(index, i * 12);
|
|
28
|
+
bytes.writeDoubleLE(value, i * 12 + 4);
|
|
29
|
+
previous = index;
|
|
30
|
+
}
|
|
31
|
+
return new Vector(vectorFromSparseEntries(dimensions, bytes));
|
|
32
|
+
}
|
|
33
|
+
static quantized8(values) { return constructVector('quantized8', values); }
|
|
34
|
+
static bit1(values) { return constructVector('bit1', values); }
|
|
35
|
+
constructor(bytes) {
|
|
36
|
+
if (!(bytes instanceof Uint8Array)) throw new TypeError('Vector requires encoded bytes');
|
|
37
|
+
this.bytes = Buffer.from(bytes);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function constructVector(encoding, values) {
|
|
41
|
+
if (!Array.isArray(values) && !(values instanceof Float32Array) && !(values instanceof Float64Array)) {
|
|
42
|
+
throw new TypeError('vector components require a number array, Float32Array or Float64Array');
|
|
43
|
+
}
|
|
44
|
+
if (values.length < 1 || values.length > 65536) throw new RangeError('vector dimensions must be 1..65536');
|
|
45
|
+
const bytes = Buffer.alloc(values.length * 8);
|
|
46
|
+
for (let i = 0; i < values.length; i++) {
|
|
47
|
+
const value = values[i];
|
|
48
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) throw new TypeError('vector components must be finite numbers');
|
|
49
|
+
if (encoding !== 'float64' && !Number.isFinite(Math.fround(value))) throw new RangeError('vector component is outside the finite float32 range');
|
|
50
|
+
bytes.writeDoubleLE(value, i * 8);
|
|
51
|
+
}
|
|
52
|
+
return new Vector(vectorFromComponents(encoding, bytes));
|
|
53
|
+
}
|
|
54
|
+
function encode(value, depth = 0) {
|
|
55
|
+
if (depth > 64) throw new RangeError('value nesting exceeds 64');
|
|
56
|
+
const tagged = (type, value) => ({ type, value });
|
|
57
|
+
if (value === null) return { type: 'Null' };
|
|
58
|
+
if (typeof value === 'boolean') return tagged('Boolean', value);
|
|
59
|
+
if (typeof value === 'string') return tagged('String', text(value));
|
|
60
|
+
if (typeof value === 'bigint') {
|
|
61
|
+
if (value < -(1n << 63n) || value >= (1n << 63n)) throw new RangeError('integer outside int64');
|
|
62
|
+
return tagged('Integer', value.toString());
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'number') {
|
|
65
|
+
if (!Number.isFinite(value)) throw new TypeError('non-finite number');
|
|
66
|
+
if (Number.isInteger(value) && !Object.is(value, -0)) {
|
|
67
|
+
if (!Number.isSafeInteger(value)) throw new RangeError('use bigint for integers outside the safe Number range');
|
|
68
|
+
return tagged('Integer', String(value));
|
|
69
|
+
}
|
|
70
|
+
const bytes = Buffer.alloc(8); bytes.writeDoubleBE(value);
|
|
71
|
+
return tagged('Number', bytes.toString('hex'));
|
|
72
|
+
}
|
|
73
|
+
if (value instanceof Record) return tagged('Record', { table: text(value.table), key: encode(value.key, depth) });
|
|
74
|
+
if (value instanceof Vector) return tagged('Vector', [...value.bytes]);
|
|
75
|
+
if (value instanceof Uint8Array) return tagged('Binary', [...value]);
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
const entries = [];
|
|
78
|
+
for (let i = 0; i < value.length; i++) {
|
|
79
|
+
if (!Object.hasOwn(value, i)) throw new TypeError('array holes are not database values');
|
|
80
|
+
entries.push(encode(value[i], depth + 1));
|
|
81
|
+
}
|
|
82
|
+
return tagged('Array', entries);
|
|
83
|
+
}
|
|
84
|
+
if (value && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
85
|
+
return tagged('Object', Object.fromEntries(Object.entries(value).map(([k,v]) => [text(k), encode(v, depth + 1)])));
|
|
86
|
+
}
|
|
87
|
+
throw new TypeError('unsupported parameter value');
|
|
88
|
+
}
|
|
89
|
+
function text(value) {
|
|
90
|
+
if (!value.isWellFormed()) throw new TypeError('strings must be well-formed Unicode');
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function decode(value) {
|
|
94
|
+
switch (value.type) {
|
|
95
|
+
case 'Null': return null;
|
|
96
|
+
case 'Boolean': case 'String': return value.value;
|
|
97
|
+
case 'Integer': return BigInt(value.value);
|
|
98
|
+
case 'Number': return Buffer.from(value.value, 'hex').readDoubleBE();
|
|
99
|
+
case 'Binary': return Buffer.from(value.value);
|
|
100
|
+
case 'Vector': return new Vector(Buffer.from(value.value));
|
|
101
|
+
case 'Record': return new Record(value.value.table, decode(value.value.key));
|
|
102
|
+
case 'Array': return value.value.map(decode);
|
|
103
|
+
case 'Object': return Object.fromEntries(Object.entries(value.value).map(([k,v]) => [k, decode(v)]));
|
|
104
|
+
default: throw new Error('unsupported native value encoding');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function unwrap(raw) {
|
|
108
|
+
const report = JSON.parse(raw);
|
|
109
|
+
if (report.version !== 1) throw new Error('unsupported native report version');
|
|
110
|
+
if (report.execution.error) {
|
|
111
|
+
const error = new Error(report.execution.error.message);
|
|
112
|
+
error.code = report.execution.error.code;
|
|
113
|
+
error.transaction = report.transaction;
|
|
114
|
+
if (report.execution.error.migration) {
|
|
115
|
+
const migration = report.execution.error.migration;
|
|
116
|
+
error.migration = {version:BigInt(migration.version),offset:BigInt(migration.offset),cause:migration.cause};
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
return report;
|
|
121
|
+
}
|
|
122
|
+
// Parsed wire rows are private to this response. Replacing cells releases their
|
|
123
|
+
// wire representation as decoding proceeds instead of retaining a second rowset.
|
|
124
|
+
function decodeRows(rows) {
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
for (let i = 0; i < row.length; i++) row[i] = decode(row[i]);
|
|
127
|
+
}
|
|
128
|
+
return rows;
|
|
129
|
+
}
|
|
130
|
+
function decodeBatch(raw) {
|
|
131
|
+
return unwrap(raw).execution.result.map(entry => {
|
|
132
|
+
if (entry.result) entry.result = { ...entry.result,
|
|
133
|
+
rows: decodeRows(entry.result.rows), affected: BigInt(entry.result.affected) };
|
|
134
|
+
return entry;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function migrationPlan(migrations) {
|
|
138
|
+
const plan = migrations.map(m => {
|
|
139
|
+
if (typeof m.version !== 'bigint' || typeof m.name !== 'string' || typeof m.sql !== 'string') {
|
|
140
|
+
throw new TypeError('migration requires bigint version, string name and SQL');
|
|
141
|
+
}
|
|
142
|
+
return { version: encode(m.version).value, name: m.name, sql: m.sql };
|
|
143
|
+
});
|
|
144
|
+
return plan;
|
|
145
|
+
}
|
|
146
|
+
function resultLimits(limits) {
|
|
147
|
+
if (limits === null || typeof limits !== 'object' || Array.isArray(limits)) throw new TypeError('result limits must be an object');
|
|
148
|
+
for (const key of Object.keys(limits)) {
|
|
149
|
+
if (key !== 'maxRows' && key !== 'maxPayloadBytes') throw new TypeError(`unknown result limit ${key}`);
|
|
150
|
+
}
|
|
151
|
+
return ['maxRows', 'maxPayloadBytes'].map(key => {
|
|
152
|
+
const value = limits[key];
|
|
153
|
+
if (typeof value !== 'bigint') throw new TypeError('result limits require both bigint fields');
|
|
154
|
+
if (value < 0n || value > 18446744073709551615n) throw new RangeError('result limits must fit uint64');
|
|
155
|
+
return value.toString();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function connectionOptions(options) {
|
|
159
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('database options must be an object');
|
|
160
|
+
for (const key of Object.keys(options)) {
|
|
161
|
+
if (key !== 'writeBufferLimits') throw new TypeError(`unknown database option ${key}`);
|
|
162
|
+
}
|
|
163
|
+
return options.writeBufferLimits === undefined ? [] : resultLimits(options.writeBufferLimits);
|
|
164
|
+
}
|
|
165
|
+
function integrityLimits(limits) {
|
|
166
|
+
if (limits === null || typeof limits !== 'object' || Array.isArray(limits)) throw new TypeError('integrity limits must be an object');
|
|
167
|
+
const fields = { maxDocuments: '', maxEncodedBytes: '' };
|
|
168
|
+
for (const [key, value] of Object.entries(limits)) {
|
|
169
|
+
if (key !== 'maxDocuments' && key !== 'maxEncodedBytes') throw new TypeError(`unknown integrity limit ${key}`);
|
|
170
|
+
if (value === undefined) continue;
|
|
171
|
+
if (typeof value !== 'bigint') throw new TypeError('integrity limits require bigint');
|
|
172
|
+
if (value < 0n || value > 18446744073709551615n) throw new RangeError('integrity limits must fit uint64');
|
|
173
|
+
fields[key] = value.toString();
|
|
174
|
+
}
|
|
175
|
+
return [fields.maxDocuments, fields.maxEncodedBytes];
|
|
176
|
+
}
|
|
177
|
+
function decodeIntegrity(raw) {
|
|
178
|
+
const report = unwrap(raw);
|
|
179
|
+
return { ...Object.fromEntries(Object.entries(report.execution.result).map(([key, value]) => [key, BigInt(value)])), transaction: report.transaction };
|
|
180
|
+
}
|
|
181
|
+
function decodeProfile(raw) {
|
|
182
|
+
const report = unwrap(raw);
|
|
183
|
+
const { result, metrics } = report.execution.result;
|
|
184
|
+
return { result: { columns: result.columns, rows: decodeRows(result.rows),
|
|
185
|
+
affected: BigInt(result.affected), transaction: report.transaction },
|
|
186
|
+
metrics: Object.fromEntries(Object.entries(metrics).map(([key, value]) => [key, BigInt(value)])) };
|
|
187
|
+
}
|
|
188
|
+
function isRomeDBError(value) {
|
|
189
|
+
if (!(value instanceof Error) || typeof value.code !== 'string' || !/^RDB_[A-Z][A-Z0-9_]*$/.test(value.code)) return false;
|
|
190
|
+
const transaction = value.transaction;
|
|
191
|
+
if (!(transaction === undefined || (transaction !== null && typeof transaction === 'object' &&
|
|
192
|
+
(transaction.before === 'autocommit' || transaction.before === 'active') &&
|
|
193
|
+
(transaction.after === 'autocommit' || transaction.after === 'active')))) return false;
|
|
194
|
+
const migration = value.migration;
|
|
195
|
+
return migration === undefined || (migration !== null && typeof migration === 'object' &&
|
|
196
|
+
typeof migration.version === 'bigint' && migration.version > 0n &&
|
|
197
|
+
typeof migration.offset === 'bigint' && migration.offset >= 0n &&
|
|
198
|
+
migration.cause !== null && typeof migration.cause === 'object' &&
|
|
199
|
+
typeof migration.cause.code === 'string' && /^RDB_[A-Z][A-Z0-9_]*$/.test(migration.cause.code) &&
|
|
200
|
+
typeof migration.cause.message === 'string');
|
|
201
|
+
}
|
|
202
|
+
exports.isRomeDBError = isRomeDBError;
|
|
203
|
+
function cardinalityError(count, transaction) {
|
|
204
|
+
const error = new RangeError(`expected exactly one row, got ${count}`);
|
|
205
|
+
error.code = 'RDB_CARDINALITY';
|
|
206
|
+
error.transaction = transaction;
|
|
207
|
+
return error;
|
|
208
|
+
}
|
|
209
|
+
function closedError(message = 'database is closed') {
|
|
210
|
+
const error = new Error(message);
|
|
211
|
+
error.code = 'RDB_CLOSED';
|
|
212
|
+
return error;
|
|
213
|
+
}
|
|
214
|
+
class Database {
|
|
215
|
+
collection(name) { return collectionClient(this, name, false); }
|
|
216
|
+
#handle; #closed = false;
|
|
217
|
+
constructor(path = ':memory:', options = {}) { this.#handle = new NativeDatabase(path, ...connectionOptions(options)); }
|
|
218
|
+
get #native() {
|
|
219
|
+
if (this.#closed) throw closedError();
|
|
220
|
+
return this.#handle;
|
|
221
|
+
}
|
|
222
|
+
close() { this.#handle.close(); this.#closed = true; }
|
|
223
|
+
execute(sql, parameters = {}) {
|
|
224
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
225
|
+
const report = unwrap(this.#native.execute(sql, JSON.stringify(params)));
|
|
226
|
+
const result = report.execution.result;
|
|
227
|
+
return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
|
|
228
|
+
}
|
|
229
|
+
profileSelect(sql, parameters = {}) {
|
|
230
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
231
|
+
return decodeProfile(this.#native.profileSelect(sql, JSON.stringify(params)));
|
|
232
|
+
}
|
|
233
|
+
selectWithLimits(sql, limits, parameters = {}) {
|
|
234
|
+
return this.profileSelectWithLimits(sql, limits, parameters).result;
|
|
235
|
+
}
|
|
236
|
+
profileSelectWithLimits(sql, limits, parameters = {}) {
|
|
237
|
+
const args = resultLimits(limits);
|
|
238
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
239
|
+
return decodeProfile(this.#native.profileSelectWithLimits(sql, JSON.stringify(params), ...args));
|
|
240
|
+
}
|
|
241
|
+
writeWithResultLimits(sql, limits, parameters = {}) {
|
|
242
|
+
const args = resultLimits(limits);
|
|
243
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
244
|
+
const report = unwrap(this.#native.writeWithResultLimits(sql, JSON.stringify(params), ...args));
|
|
245
|
+
const result = report.execution.result;
|
|
246
|
+
return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
|
|
247
|
+
}
|
|
248
|
+
checkCollectionIntegrity(table, limits = {}) {
|
|
249
|
+
return decodeIntegrity(this.#native.checkCollectionIntegrity(table, ...integrityLimits(limits)));
|
|
250
|
+
}
|
|
251
|
+
executeBatch(script) { return decodeBatch(this.#native.executeBatch(script)); }
|
|
252
|
+
exportDocuments(table, format = 'json') {
|
|
253
|
+
return unwrap(this.#native.exportDocuments(table, format)).execution.result;
|
|
254
|
+
}
|
|
255
|
+
importDocuments(table, input, format = 'json') {
|
|
256
|
+
const report = unwrap(this.#native.importDocuments(table, input, format));
|
|
257
|
+
return { ...report.execution.result, transaction: report.transaction };
|
|
258
|
+
}
|
|
259
|
+
migrate(migrations) {
|
|
260
|
+
const plan = migrationPlan(migrations);
|
|
261
|
+
const report = unwrap(this.#native.migrate(JSON.stringify(plan)));
|
|
262
|
+
return { alreadyApplied: report.execution.result.alreadyApplied,
|
|
263
|
+
applied: report.execution.result.applied.map(BigInt), transaction: report.transaction };
|
|
264
|
+
}
|
|
265
|
+
all(sql, parameters) { return this.execute(sql, parameters).rows; }
|
|
266
|
+
first(sql, parameters) { return this.all(sql, parameters)[0]; }
|
|
267
|
+
exactlyOne(sql, parameters) {
|
|
268
|
+
const { rows, transaction } = this.execute(sql, parameters);
|
|
269
|
+
if (rows.length !== 1) throw cardinalityError(rows.length, transaction);
|
|
270
|
+
return rows[0];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
exports.Database = Database;
|
|
274
|
+
exports.Record = Record;
|
|
275
|
+
exports.Vector = Vector;
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
const asyncConstruction = Symbol('AsyncDatabase');
|
|
279
|
+
class AsyncDatabase {
|
|
280
|
+
collection(name) { return collectionClient(this, name, true); }
|
|
281
|
+
#interruptKey;
|
|
282
|
+
#worker; #pending = new Map(); #next = 0; #bytes = 0;
|
|
283
|
+
#ready; #readyResolve; #readyReject; #exited;
|
|
284
|
+
#failure; #closing = false; #closePromise; #stopped = false; #shutdownRequested = false;
|
|
285
|
+
constructor(path, token, limits) {
|
|
286
|
+
if (token !== asyncConstruction) throw new TypeError('use AsyncDatabase.open()');
|
|
287
|
+
const { Worker } = require('node:worker_threads');
|
|
288
|
+
this.#ready = new Promise((resolve, reject) => { this.#readyResolve = resolve; this.#readyReject = reject; });
|
|
289
|
+
this.#worker = new Worker(require.resolve('./worker.cjs'), { workerData: { path, limits } });
|
|
290
|
+
this.#exited = new Promise(resolve => this.#worker.once('exit', code => {
|
|
291
|
+
this.#stopped = true;
|
|
292
|
+
if (!this.#closing || this.#pending.size) this.#fail(new Error(`database worker exited (${code})`));
|
|
293
|
+
resolve();
|
|
294
|
+
}));
|
|
295
|
+
this.#worker.on('error', error => this.#fail(error));
|
|
296
|
+
this.#worker.on('messageerror', error => this.#fail(error, true));
|
|
297
|
+
this.#worker.on('message', message => {
|
|
298
|
+
if (message.ready) { this.#interruptKey = message.interruptKey; this.#readyResolve(); return; }
|
|
299
|
+
const pending = this.#pending.get(message.id);
|
|
300
|
+
if (!pending) return;
|
|
301
|
+
this.#pending.delete(message.id); this.#bytes -= pending.bytes; pending.cleanup();
|
|
302
|
+
if (message.error) pending.reject(new Error(message.error.message));
|
|
303
|
+
else pending.resolve(message.result);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
interrupt() { return this.#interruptKey !== undefined && interruptConnection(this.#interruptKey); }
|
|
307
|
+
static async open(path = ':memory:', options = {}) {
|
|
308
|
+
const limits = connectionOptions(options);
|
|
309
|
+
const db = new AsyncDatabase(path, asyncConstruction, limits);
|
|
310
|
+
try { await db.#ready; return db; }
|
|
311
|
+
catch (error) { await db.#exited; throw error; }
|
|
312
|
+
}
|
|
313
|
+
#fail(error, shutdown = false) {
|
|
314
|
+
if (!this.#failure) {
|
|
315
|
+
this.#failure = new Error(`database worker failure: ${error.message}`, { cause: error });
|
|
316
|
+
this.#failure.code = 'RDB_WORKER';
|
|
317
|
+
}
|
|
318
|
+
this.#readyReject(this.#failure);
|
|
319
|
+
for (const pending of this.#pending.values()) { pending.cleanup(); pending.reject(this.#failure); }
|
|
320
|
+
this.#pending.clear(); this.#bytes = 0;
|
|
321
|
+
if (shutdown && !this.#stopped && !this.#shutdownRequested) {
|
|
322
|
+
this.#shutdownRequested = true;
|
|
323
|
+
// The response channel is damaged. Ask the worker to close after the
|
|
324
|
+
// accepted queue; their write outcomes can no longer be inferred here.
|
|
325
|
+
try { this.#worker.postMessage({ id: 0, method: 'close', args: [] }); }
|
|
326
|
+
catch { void this.#worker.terminate(); }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
#request(method, args, closing = false, signal, timeoutMs) {
|
|
330
|
+
if (this.#failure) return Promise.reject(this.#failure);
|
|
331
|
+
if (this.#closing && !closing) return Promise.reject(closedError('database is closing or closed'));
|
|
332
|
+
const bytes = args.reduce((size, arg) => size + Buffer.byteLength(arg), 0);
|
|
333
|
+
if (!closing && (this.#pending.size >= 256 || this.#bytes + bytes > 128 * 1024 * 1024)) {
|
|
334
|
+
const error = new RangeError('database worker queue limit exceeded'); error.code = 'RDB_LIMIT';
|
|
335
|
+
return Promise.reject(error);
|
|
336
|
+
}
|
|
337
|
+
const id = ++this.#next;
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
let cancellationKey, listener;
|
|
340
|
+
const cleanup = () => {
|
|
341
|
+
listener?.[Symbol.dispose]();
|
|
342
|
+
if (cancellationKey !== undefined) releaseCancellationToken(cancellationKey);
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 4294967295)) {
|
|
346
|
+
throw new RangeError('timeoutMs must be an integer from 0 through 4294967295');
|
|
347
|
+
}
|
|
348
|
+
if (signal !== undefined || timeoutMs !== undefined) {
|
|
349
|
+
cancellationKey = createCancellationToken(timeoutMs === undefined ? undefined : String(timeoutMs));
|
|
350
|
+
}
|
|
351
|
+
if (signal !== undefined) {
|
|
352
|
+
listener = require('node:events').addAbortListener(signal, () => cancelOperation(cancellationKey));
|
|
353
|
+
if (signal.aborted) cancelOperation(cancellationKey);
|
|
354
|
+
}
|
|
355
|
+
} catch (error) { cleanup(); reject(error); return; }
|
|
356
|
+
this.#pending.set(id, { resolve, reject, bytes, cleanup }); this.#bytes += bytes;
|
|
357
|
+
try { this.#worker.postMessage({ id, method, args, cancellationKey }); }
|
|
358
|
+
catch (error) { this.#pending.delete(id); this.#bytes -= bytes; cleanup(); reject(error); }
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
close() {
|
|
362
|
+
if (!this.#closePromise) {
|
|
363
|
+
this.#closing = true;
|
|
364
|
+
this.#closePromise = this.#failure ? this.#exited : this.#request('close', [], true)
|
|
365
|
+
.then(() => this.#exited)
|
|
366
|
+
.catch(async error => {
|
|
367
|
+
this.#fail(error, true);
|
|
368
|
+
await this.#exited;
|
|
369
|
+
throw this.#failure;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return this.#closePromise;
|
|
373
|
+
}
|
|
374
|
+
async execute(sql, parameters = {}, options = {}) {
|
|
375
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
376
|
+
const report = unwrap(await this.#request('execute', [sql, JSON.stringify(params)], false, options.signal, options.timeoutMs));
|
|
377
|
+
const result = report.execution.result;
|
|
378
|
+
return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
|
|
379
|
+
}
|
|
380
|
+
async profileSelect(sql, parameters = {}, options = {}) {
|
|
381
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
382
|
+
return decodeProfile(await this.#request('profileSelect', [sql, JSON.stringify(params)], false, options.signal, options.timeoutMs));
|
|
383
|
+
}
|
|
384
|
+
async selectWithLimits(sql, limits, parameters = {}, options = {}) {
|
|
385
|
+
return (await this.profileSelectWithLimits(sql, limits, parameters, options)).result;
|
|
386
|
+
}
|
|
387
|
+
async profileSelectWithLimits(sql, limits, parameters = {}, options = {}) {
|
|
388
|
+
const args = resultLimits(limits);
|
|
389
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
390
|
+
return decodeProfile(await this.#request('profileSelectWithLimits', [sql, JSON.stringify(params), ...args], false, options.signal, options.timeoutMs));
|
|
391
|
+
}
|
|
392
|
+
async writeWithResultLimits(sql, limits, parameters = {}, options = {}) {
|
|
393
|
+
const args = resultLimits(limits);
|
|
394
|
+
const params = Object.fromEntries(Object.entries(parameters).map(([k,v]) => [text(k), encode(v)]));
|
|
395
|
+
const report = unwrap(await this.#request('writeWithResultLimits', [sql, JSON.stringify(params), ...args], false, options.signal, options.timeoutMs));
|
|
396
|
+
const result = report.execution.result;
|
|
397
|
+
return { columns: result.columns, rows: decodeRows(result.rows), affected: BigInt(result.affected), transaction: report.transaction };
|
|
398
|
+
}
|
|
399
|
+
async checkCollectionIntegrity(table, limits = {}, options = {}) {
|
|
400
|
+
return decodeIntegrity(await this.#request('checkCollectionIntegrity', [table, ...integrityLimits(limits)], false, options.signal, options.timeoutMs));
|
|
401
|
+
}
|
|
402
|
+
async executeBatch(script, options = {}) { return decodeBatch(await this.#request('executeBatch', [script], false, options.signal, options.timeoutMs)); }
|
|
403
|
+
async exportDocuments(table, format = 'json', options = {}) {
|
|
404
|
+
return unwrap(await this.#request('exportDocuments', [table, format], false, options.signal, options.timeoutMs)).execution.result;
|
|
405
|
+
}
|
|
406
|
+
async importDocuments(table, input, format = 'json', options = {}) {
|
|
407
|
+
const report = unwrap(await this.#request('importDocuments', [table, input, format], false, options.signal, options.timeoutMs));
|
|
408
|
+
return { ...report.execution.result, transaction: report.transaction };
|
|
409
|
+
}
|
|
410
|
+
async migrate(migrations, options = {}) {
|
|
411
|
+
const report = unwrap(await this.#request('migrate', [JSON.stringify(migrationPlan(migrations))], false, options.signal, options.timeoutMs));
|
|
412
|
+
return { alreadyApplied: report.execution.result.alreadyApplied,
|
|
413
|
+
applied: report.execution.result.applied.map(BigInt), transaction: report.transaction };
|
|
414
|
+
}
|
|
415
|
+
async exportFile(destination, options = {}) {
|
|
416
|
+
if (typeof destination !== 'string' || !destination || destination.length > 4096 || destination.includes('\0')) throw new TypeError('exportFile requires a bounded destination path');
|
|
417
|
+
try { return await this.#request('exportFile', [require('node:path').resolve(destination)], false, options.signal, options.timeoutMs); }
|
|
418
|
+
catch (error) {
|
|
419
|
+
const coded = /^(RDB_[A-Z0-9_]+): (.*)$/s.exec(error.message);
|
|
420
|
+
if (!coded) throw error;
|
|
421
|
+
const failure = new Error(coded[2]);
|
|
422
|
+
failure.code = coded[1];
|
|
423
|
+
throw failure;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async all(sql, parameters, options) { return (await this.execute(sql, parameters, options)).rows; }
|
|
427
|
+
async first(sql, parameters, options) { return (await this.all(sql, parameters, options))[0]; }
|
|
428
|
+
async exactlyOne(sql, parameters, options) {
|
|
429
|
+
const { rows, transaction } = await this.execute(sql, parameters, options);
|
|
430
|
+
if (rows.length !== 1) throw cardinalityError(rows.length, transaction);
|
|
431
|
+
return rows[0];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
exports.AsyncDatabase = AsyncDatabase;
|
|
435
|
+
|
|
436
|
+
// Collection identifiers are quoted; document values and record keys are bound.
|
|
437
|
+
function collectionClient(db, name, asynchronous) {
|
|
438
|
+
if (typeof name !== 'string' || !name.length || name.includes('\0')) throw new TypeError('collection name must be a nonempty string without NUL');
|
|
439
|
+
const quote = value => '"' + value.replaceAll('"', '""') + '"';
|
|
440
|
+
const table = quote(name);
|
|
441
|
+
const record = key => new Record(name, key);
|
|
442
|
+
const document = value => {
|
|
443
|
+
if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new TypeError('expected a document object');
|
|
444
|
+
return value;
|
|
445
|
+
};
|
|
446
|
+
const fields = value => {
|
|
447
|
+
document(value);
|
|
448
|
+
if (Object.hasOwn(value, 'id')) throw new TypeError('pass the record key separately; document must omit id');
|
|
449
|
+
return Object.entries(value);
|
|
450
|
+
};
|
|
451
|
+
const run = (sql, params, options, many = false) => {
|
|
452
|
+
const project = result => many ? result.rows.map(row => row[0]) : result.rows[0]?.[0];
|
|
453
|
+
const check = info => {
|
|
454
|
+
if (info.rows[0]?.[0]?.model !== 'document') throw new TypeError('collection methods require a document collection');
|
|
455
|
+
};
|
|
456
|
+
const missing = error => { if (error.code !== 'RDB_NOT_FOUND') throw error; };
|
|
457
|
+
if (asynchronous) return (async () => {
|
|
458
|
+
try { check(await db.execute(`INFO FOR TABLE ${table}`, {}, options)); } catch (error) { missing(error); }
|
|
459
|
+
return project(await db.execute(sql, params, options));
|
|
460
|
+
})();
|
|
461
|
+
try { check(db.execute(`INFO FOR TABLE ${table}`)); } catch (error) { missing(error); }
|
|
462
|
+
return project(db.execute(sql, params));
|
|
463
|
+
};
|
|
464
|
+
return Object.freeze({
|
|
465
|
+
all(options) { return run(`SELECT * FROM ${table}`, {}, options, true); },
|
|
466
|
+
get(key, options) { return run(`SELECT * FROM ${table} WHERE id=$id`, {$id:record(key)}, options); },
|
|
467
|
+
insert(value, options) { return run(`INSERT INTO ${table} DOCUMENT $doc RETURNING *`, {$doc:document(value)}, options); },
|
|
468
|
+
upsert(key, value, options) {
|
|
469
|
+
const params = {$id:record(key)};
|
|
470
|
+
const body = fields(value).map(([field, v], i) => { params[`$v${i}`] = v; return `${quote(field)}:$v${i}`; });
|
|
471
|
+
return run(`UPSERT ${table} {id:$id${body.length ? ',' + body.join(',') : ''}} RETURNING *`, params, options);
|
|
472
|
+
},
|
|
473
|
+
merge(key, value, options) {
|
|
474
|
+
const params = {$id:record(key)};
|
|
475
|
+
const sets = fields(value).map(([field, v], i) => { params[`$v${i}`] = v; return `${quote(field)}=$v${i}`; });
|
|
476
|
+
return sets.length ? run(`UPDATE ${table} SET ${sets.join(',')} WHERE id=$id RETURNING *`, params, options)
|
|
477
|
+
: run(`SELECT * FROM ${table} WHERE id=$id`, params, options);
|
|
478
|
+
},
|
|
479
|
+
delete(key, options) { return run(`DELETE FROM ${table} WHERE id=$id RETURNING *`, {$id:record(key)}, options); },
|
|
480
|
+
});
|
|
481
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export class Record { readonly table: string; readonly key: string | bigint; constructor(table: string, key: string | bigint); }
|
|
2
|
+
export type SparseVectorEntry = readonly [index: number, value: number];
|
|
3
|
+
export type VectorComponents = readonly number[] | Float32Array | Float64Array;
|
|
4
|
+
export class Vector {
|
|
5
|
+
readonly bytes: Uint8Array;
|
|
6
|
+
constructor(bytes: Uint8Array);
|
|
7
|
+
static float32(values: VectorComponents): Vector;
|
|
8
|
+
static float64(values: VectorComponents): Vector;
|
|
9
|
+
static sparse32(values: VectorComponents): Vector;
|
|
10
|
+
static sparse32Entries(dimensions: number, entries: readonly SparseVectorEntry[]): Vector;
|
|
11
|
+
static quantized8(values: VectorComponents): Vector;
|
|
12
|
+
static bit1(values: VectorComponents): Vector;
|
|
13
|
+
}
|
|
14
|
+
export type Value = null | boolean | string | bigint | number | Uint8Array | Record | Vector | Value[] | { [field: string]: Value };
|
|
15
|
+
export interface Parameters { [name: string]: Value; }
|
|
16
|
+
export interface Transaction { before: 'autocommit' | 'active'; after: 'autocommit' | 'active'; }
|
|
17
|
+
export interface MigrationFailure { version: bigint; offset: bigint; cause: {code: string; message: string}; }
|
|
18
|
+
export interface RomeDBError extends Error { code: string; transaction?: Transaction; migration?: MigrationFailure; }
|
|
19
|
+
/** Recognizes coded RomeDB errors and validates optional transaction and migration details. */
|
|
20
|
+
export function isRomeDBError(value: unknown): value is RomeDBError;
|
|
21
|
+
export interface QueryResult { columns: string[]; rows: Value[][]; affected: bigint; transaction: Transaction; }
|
|
22
|
+
/** Retained logical result limits; excludes engine working memory and temporary decoding. */
|
|
23
|
+
export interface DatabaseOptions { writeBufferLimits?: ResultLimits; }
|
|
24
|
+
export interface ResultLimits { maxRows: bigint; maxPayloadBytes: bigint; }
|
|
25
|
+
export interface IntegrityLimits { maxDocuments?: bigint; maxEncodedBytes?: bigint; }
|
|
26
|
+
export interface IntegrityReport {
|
|
27
|
+
documents: bigint; indexes: bigint; indexEntries: bigint; encodedBytes: bigint; transaction: Transaction;
|
|
28
|
+
}
|
|
29
|
+
export interface QueryMetrics {
|
|
30
|
+
rowsRead: bigint; rowsWritten: bigint; fullscanSteps: bigint; indexSteps: bigint;
|
|
31
|
+
vmSteps: bigint; sortOperations: bigint; btreeSeeks: bigint;
|
|
32
|
+
fetchBatches: bigint; fetchRowsRead: bigint; fetchVmSteps: bigint;
|
|
33
|
+
/** GRAPH source work: adjacency entries read, expansions, predicate documents, rows, visited nodes and peak state bytes. */
|
|
34
|
+
graphAdjacencyEntries: bigint; graphExpansions: bigint; graphDocuments: bigint;
|
|
35
|
+
graphRows: bigint; graphVisited: bigint; graphPeakBytes: bigint;
|
|
36
|
+
/** Spatial work: index key ranges searched and nearest radius rounds. */
|
|
37
|
+
spatialRanges: bigint; spatialRounds: bigint;
|
|
38
|
+
}
|
|
39
|
+
export interface ProfiledQuery { result: QueryResult; metrics: QueryMetrics; }
|
|
40
|
+
export type BatchExecution = { offset: number; transaction: Transaction } & (
|
|
41
|
+
{ result: Omit<QueryResult, 'transaction'>; error?: never } |
|
|
42
|
+
{ error: { code: string; message: string }; result?: never }
|
|
43
|
+
);
|
|
44
|
+
export type TransferFormat = 'json' | 'ndjson';
|
|
45
|
+
export interface Migration { version: bigint; name: string; sql: string; }
|
|
46
|
+
export interface MigrationReport { alreadyApplied: number; applied: bigint[]; transaction: Transaction; }
|
|
47
|
+
export interface ImportReport { imported: number; transaction: Transaction; }
|
|
48
|
+
export class Database {
|
|
49
|
+
collection<T extends object = Document>(name: string): Collection<T>;
|
|
50
|
+
constructor(path?: string, options?: DatabaseOptions);
|
|
51
|
+
close(): void;
|
|
52
|
+
migrate(migrations: Migration[]): MigrationReport;
|
|
53
|
+
exportDocuments(table: string, format?: TransferFormat): string;
|
|
54
|
+
importDocuments(table: string, input: string, format?: TransferFormat): ImportReport;
|
|
55
|
+
execute(sql: string, parameters?: Parameters): QueryResult;
|
|
56
|
+
profileSelect(sql: string, parameters?: Parameters): ProfiledQuery;
|
|
57
|
+
/** Checks write results atomically; does not bound engine or logical materialization memory. */
|
|
58
|
+
writeWithResultLimits(sql: string, limits: ResultLimits, parameters?: Parameters): QueryResult;
|
|
59
|
+
selectWithLimits(sql: string, limits: ResultLimits, parameters?: Parameters): QueryResult;
|
|
60
|
+
profileSelectWithLimits(sql: string, limits: ResultLimits, parameters?: Parameters): ProfiledQuery;
|
|
61
|
+
checkCollectionIntegrity(table: string, limits?: IntegrityLimits): IntegrityReport;
|
|
62
|
+
executeBatch(script: string): BatchExecution[];
|
|
63
|
+
all(sql: string, parameters?: Parameters): Value[][];
|
|
64
|
+
first(sql: string, parameters?: Parameters): Value[] | undefined;
|
|
65
|
+
exactlyOne(sql: string, parameters?: Parameters): Value[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ExecuteOptions { signal?: AbortSignal; timeoutMs?: number; }
|
|
69
|
+
export interface FileExport { bytes: number; }
|
|
70
|
+
export class AsyncDatabase {
|
|
71
|
+
collection<T extends object = Document>(name: string): AsyncCollection<T>;
|
|
72
|
+
private constructor();
|
|
73
|
+
static open(path?: string, options?: DatabaseOptions): Promise<AsyncDatabase>;
|
|
74
|
+
interrupt(): boolean;
|
|
75
|
+
close(): Promise<void>;
|
|
76
|
+
migrate(migrations: Migration[], options?: ExecuteOptions): Promise<MigrationReport>;
|
|
77
|
+
exportDocuments(table: string, format?: TransferFormat, options?: ExecuteOptions): Promise<string>;
|
|
78
|
+
importDocuments(table: string, input: string, format?: TransferFormat, options?: ExecuteOptions): Promise<ImportReport>;
|
|
79
|
+
/** Copy an MVCC database into a new private file while queries continue; the destination and its log files must not exist. */
|
|
80
|
+
exportFile(destination: string, options?: ExecuteOptions): Promise<FileExport>;
|
|
81
|
+
execute(sql: string, parameters?: Parameters, options?: ExecuteOptions): Promise<QueryResult>;
|
|
82
|
+
profileSelect(sql: string, parameters?: Parameters, options?: ExecuteOptions): Promise<ProfiledQuery>;
|
|
83
|
+
/** Checks write results atomically; does not bound engine or logical materialization memory. */
|
|
84
|
+
writeWithResultLimits(sql: string, limits: ResultLimits, parameters?: Parameters, options?: ExecuteOptions): Promise<QueryResult>;
|
|
85
|
+
selectWithLimits(sql: string, limits: ResultLimits, parameters?: Parameters, options?: ExecuteOptions): Promise<QueryResult>;
|
|
86
|
+
profileSelectWithLimits(sql: string, limits: ResultLimits, parameters?: Parameters, options?: ExecuteOptions): Promise<ProfiledQuery>;
|
|
87
|
+
checkCollectionIntegrity(table: string, limits?: IntegrityLimits, options?: ExecuteOptions): Promise<IntegrityReport>;
|
|
88
|
+
executeBatch(script: string, options?: ExecuteOptions): Promise<BatchExecution[]>;
|
|
89
|
+
all(sql: string, parameters?: Parameters, options?: ExecuteOptions): Promise<Value[][]>;
|
|
90
|
+
first(sql: string, parameters?: Parameters, options?: ExecuteOptions): Promise<Value[] | undefined>;
|
|
91
|
+
exactlyOne(sql: string, parameters?: Parameters, options?: ExecuteOptions): Promise<Value[]>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type Document = { [field: string]: Value };
|
|
95
|
+
export type StoredDocument<T> = Omit<T, 'id'> & { id: Record };
|
|
96
|
+
/** Generic types describe application expectations; use field validation for runtime enforcement. */
|
|
97
|
+
export interface Collection<T extends object = Document> {
|
|
98
|
+
all(): StoredDocument<T>[];
|
|
99
|
+
get(key: string | bigint): StoredDocument<T> | undefined;
|
|
100
|
+
insert(value: T): StoredDocument<T>;
|
|
101
|
+
upsert(key: string | bigint, value: Partial<Omit<T, 'id'>>): StoredDocument<T>;
|
|
102
|
+
merge(key: string | bigint, value: Partial<Omit<T, 'id'>>): StoredDocument<T> | undefined;
|
|
103
|
+
delete(key: string | bigint): StoredDocument<T> | undefined;
|
|
104
|
+
}
|
|
105
|
+
export interface AsyncCollection<T extends object = Document> {
|
|
106
|
+
all(options?: ExecuteOptions): Promise<StoredDocument<T>[]>;
|
|
107
|
+
get(key: string | bigint, options?: ExecuteOptions): Promise<StoredDocument<T> | undefined>;
|
|
108
|
+
insert(value: T, options?: ExecuteOptions): Promise<StoredDocument<T>>;
|
|
109
|
+
upsert(key: string | bigint, value: Partial<Omit<T, 'id'>>, options?: ExecuteOptions): Promise<StoredDocument<T>>;
|
|
110
|
+
merge(key: string | bigint, value: Partial<Omit<T, 'id'>>, options?: ExecuteOptions): Promise<StoredDocument<T> | undefined>;
|
|
111
|
+
delete(key: string | bigint, options?: ExecuteOptions): Promise<StoredDocument<T> | undefined>;
|
|
112
|
+
}
|
package/native.cjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const minimumNodeMajor = 22;
|
|
3
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
4
|
+
if (nodeMajor < minimumNodeMajor) {
|
|
5
|
+
const error = new Error(`RomeDB requires Node.js ${minimumNodeMajor} or newer; this process is running Node ${process.versions.node}.`);
|
|
6
|
+
error.code = 'RDB_RUNTIME_VERSION';
|
|
7
|
+
throw error;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
module.exports = require('./romedb.node');
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
const error = new Error(
|
|
13
|
+
`RomeDB could not load its native addon for ${process.platform}/${process.arch} on Node ${process.versions.node}. ` +
|
|
14
|
+
'Use an addon built for this platform; for a source checkout, run romedb/scripts/check-node.sh from the repository root. ' +
|
|
15
|
+
'See the package README for supported build and installation instructions.',
|
|
16
|
+
{ cause },
|
|
17
|
+
);
|
|
18
|
+
error.code = 'RDB_NATIVE_LOAD';
|
|
19
|
+
throw error;
|
|
20
|
+
}
|