@bullet./paraql 0.1.0

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/lib/vfs.js ADDED
@@ -0,0 +1,558 @@
1
+ const Autobee = require("autobee")
2
+ const { Buffer } = require("buffer")
3
+ const fs = require("fs/promises")
4
+ const path = require("path")
5
+ const ReadyResource = require("ready-resource")
6
+ const RocksDB = require("rocksdb-native")
7
+
8
+ const binding = require("../binding")
9
+ const { PageKey, Operation, deflate, inflate } = require("./codecs")
10
+ const { OPERATION, PAGE_SIZE } = require("./constants")
11
+ const Deferred = require("./deferred")
12
+ const Encryption = require("./encryption")
13
+ const errors = require("./errors")
14
+
15
+ module.exports = class ParaVFS extends ReadyResource {
16
+ constructor(db, store, key, options) {
17
+ const {
18
+ name = "paraql.db",
19
+ keyPair = null,
20
+ encrypted = false,
21
+ encryptionKey = null,
22
+ compressed = false,
23
+ compressionLevel = 6,
24
+ } = options
25
+
26
+ super()
27
+
28
+ this._handle = null
29
+ this._db = db
30
+ this._name = name
31
+
32
+ this._encryption = null
33
+ this._compressed = compressed
34
+ this._compressionLevel = compressionLevel
35
+
36
+ this.store = store.namespace(name)
37
+ this.bee = new Autobee(this.store, key, {
38
+ apply: this._apply.bind(this),
39
+ keyPair,
40
+ encrypted,
41
+ encryptionKey,
42
+ optimistic: false,
43
+ })
44
+ this.tmp = new RocksDB(path.resolve(this.store.storage.path, name))
45
+
46
+ this._view = null
47
+ this._files = new Map()
48
+ this._interactive = null
49
+ }
50
+
51
+ get name() {
52
+ return this._name
53
+ }
54
+
55
+ get key() {
56
+ return this.bee.key
57
+ }
58
+
59
+ get local() {
60
+ return this.bee.local.key
61
+ }
62
+
63
+ get discoveryKey() {
64
+ return this.bee.discoveryKey
65
+ }
66
+
67
+ get encryptionKey() {
68
+ return this.bee.encryptionKey
69
+ }
70
+
71
+ get writable() {
72
+ return this.bee.writable
73
+ }
74
+
75
+ get encrypted() {
76
+ return !!this._encryption
77
+ }
78
+
79
+ get compressed() {
80
+ return !!this._compressed
81
+ }
82
+
83
+ async _open() {
84
+ await this.bee.ready()
85
+
86
+ this._encryption =
87
+ this.bee.encryptionKey && new Encryption(this.key, this.bee.encryptionKey)
88
+ this._handle = binding.vfsInit(
89
+ this,
90
+ this._xDelete,
91
+ this._xAccess,
92
+ this._xRead,
93
+ this._xWrite,
94
+ this._xTruncate,
95
+ this._xSync,
96
+ this._xSize,
97
+ )
98
+ }
99
+
100
+ async _close() {
101
+ if (this._handle) {
102
+ binding.vfsDestroy(this._handle)
103
+
104
+ this._handle = null
105
+ }
106
+
107
+ for (const batch of this._files.values()) {
108
+ batch.close()
109
+ }
110
+
111
+ await this.bee.close()
112
+ await this.tmp.close()
113
+ }
114
+
115
+ async _apply(nodes, view, host) {
116
+ let stmt = null
117
+ let changes = 0
118
+ let lastInsertRowid = 0
119
+ let errors = 0
120
+
121
+ for (const node of nodes) {
122
+ const op = Operation.decode(this.compressed ? await inflate(node.value) : node.value)
123
+
124
+ switch (op.type) {
125
+ case OPERATION.WRITER_ADD: {
126
+ const batch = view.write()
127
+ host.addWriter(op.key)
128
+ await batch.flush()
129
+ break
130
+ }
131
+ case OPERATION.WRITER_DEL: {
132
+ const batch = view.write()
133
+ host.removeWriter(op.key)
134
+ await batch.flush()
135
+ break
136
+ }
137
+ case OPERATION.EXEC: {
138
+ this._write(view)
139
+ try {
140
+ await this._db._exec(op.sql)
141
+ this._interactive?.resolve()
142
+ } catch (err) {
143
+ this._interactive?.reject(err)
144
+ }
145
+ this._write(null)
146
+ break
147
+ }
148
+ case OPERATION.RUN: {
149
+ this._write(view)
150
+ try {
151
+ stmt = await this._db.prepare(op.sql)
152
+ const result = await stmt._run(op.named, op.positional)
153
+ this._interactive?.resolve(result)
154
+ } catch (err) {
155
+ this._interactive?.reject(err)
156
+ }
157
+ this._write(null)
158
+ break
159
+ }
160
+ case OPERATION.STMT: {
161
+ try {
162
+ stmt = await this._db.prepare(op.sql)
163
+ } catch (err) {
164
+ throw errors.INTERNAL(`Received invalid statement: ${op.sql}`)
165
+ }
166
+ }
167
+ case OPERATION.BATCH: {
168
+ this._write(view)
169
+ try {
170
+ const result = await stmt._run(op.named, op.positional)
171
+ changes += result.changes
172
+ lastInsertRowid = result.lastInsertRowid
173
+ } catch (err) {
174
+ errors++
175
+ }
176
+ this._write(null)
177
+ break
178
+ }
179
+ case OPERATION.FLUSH: {
180
+ this._interactive?.resolve({ changes, lastInsertRowid, errors })
181
+ break
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ _write(view = null) {
188
+ if (this._files.size) {
189
+ throw errors.INTERNAL(`Unflushed batches: ${this._files.size}`)
190
+ }
191
+
192
+ this._view = view
193
+ }
194
+
195
+ async addWriter(key) {
196
+ await this.bee.append(
197
+ this.compressed
198
+ ? await deflate(Operation.encode({ type: OPERATION.WRITER_ADD, key }), {
199
+ level: this._compressionLevel,
200
+ })
201
+ : Operation.encode({ type: OPERATION.WRITER_ADD, key }),
202
+ )
203
+ }
204
+
205
+ async removeWriter(key) {
206
+ await this.bee.append(
207
+ this.compressed
208
+ ? await deflate(Operation.encode({ type: OPERATION.WRITER_DEL, key }), {
209
+ level: this._compressionLevel,
210
+ })
211
+ : Operation.encode({ type: OPERATION.WRITER_DEL, key }),
212
+ )
213
+ }
214
+
215
+ replicate(isInitiator) {
216
+ return this.bee.replicate(isInitiator)
217
+ }
218
+
219
+ async compact() {
220
+ this.bee.bee.cache.empty()
221
+ await this.bee.bee.core.startMarking()
222
+
223
+ for await (const _ of this.bee.bee.createReadStream()) {
224
+ }
225
+
226
+ await this.bee.bee.core.sweep()
227
+
228
+ await this.bee.store.storage.db.compact()
229
+
230
+ await this.tmp.compact()
231
+ }
232
+
233
+ async info() {
234
+ const storage = this.store.storage.path
235
+
236
+ let database = 0
237
+ let temporary = 0
238
+
239
+ for (const entry of await fs.readdir(storage, { encoding: "utf-8", recursive: true })) {
240
+ const stat = await fs.stat(path.resolve(storage, entry))
241
+
242
+ if (path.dirname(entry) === this.name) {
243
+ temporary += stat.size
244
+ } else {
245
+ database += stat.size
246
+ }
247
+ }
248
+
249
+ return { database, temporary, total: database + temporary }
250
+ }
251
+
252
+ async exec(sql) {
253
+ this._interactive = new Deferred()
254
+ try {
255
+ const promise = this.bee.append(
256
+ this.compressed
257
+ ? await deflate(Operation.encode({ type: OPERATION.EXEC, sql }), {
258
+ level: this._compressionLevel,
259
+ })
260
+ : Operation.encode({ type: OPERATION.EXEC, sql }),
261
+ )
262
+ await this._interactive.promise
263
+ .then(() => promise)
264
+ .catch(async (err) => {
265
+ await promise
266
+ throw err
267
+ })
268
+ } finally {
269
+ this._interactive = null
270
+ }
271
+ }
272
+
273
+ async run(sql, named, positional) {
274
+ this._interactive = new Deferred()
275
+ try {
276
+ const promise = this.bee.append(
277
+ this.compressed
278
+ ? await deflate(Operation.encode({ type: OPERATION.RUN, sql, named, positional }), {
279
+ level: this._compressionLevel,
280
+ })
281
+ : Operation.encode({ type: OPERATION.RUN, sql, named, positional }),
282
+ )
283
+ return await this._interactive.promise
284
+ .then(async (result) => {
285
+ await promise
286
+ return result
287
+ })
288
+ .catch(async (err) => {
289
+ await promise
290
+ throw err
291
+ })
292
+ } finally {
293
+ this._interactive = null
294
+ }
295
+ }
296
+
297
+ async flush(sql, operations) {
298
+ this._interactive = new Deferred()
299
+ try {
300
+ const promise = this.bee.append(
301
+ this.compressed
302
+ ? await Promise.all(
303
+ [
304
+ { type: OPERATION.STMT, sql },
305
+ ...operations.map((op) => ({ type: OPERATION.BATCH, ...op })),
306
+ { type: OPERATION.FLUSH },
307
+ ].map((op) => deflate(Operation.encode(op), { level: this._compressionLevel })),
308
+ )
309
+ : [
310
+ { type: OPERATION.STMT, sql },
311
+ ...operations.map((op) => ({ type: OPERATION.BATCH, ...op })),
312
+ { type: OPERATION.FLUSH },
313
+ ].map(Operation.encode),
314
+ )
315
+ return await this._interactive.promise
316
+ .then(async (result) => {
317
+ await promise
318
+ return result
319
+ })
320
+ .catch(async (err) => {
321
+ await promise
322
+ throw err
323
+ })
324
+ } finally {
325
+ this._interactive = null
326
+ }
327
+ }
328
+
329
+ async _get(name, index) {
330
+ const tmp = name !== this.name
331
+ const key = PageKey.encode([name, index])
332
+ let value = null
333
+
334
+ if (tmp) {
335
+ value = await this.tmp.get(key)
336
+
337
+ if (value && this.encrypted) {
338
+ value = this._encryption.decrypt(value, name, index)
339
+ }
340
+ } else {
341
+ const entry = await (this._view ?? this.bee.view).get(key)
342
+
343
+ value = entry?.value ?? null
344
+ }
345
+
346
+ if (value && this.compressed) {
347
+ value = await inflate(value)
348
+ }
349
+
350
+ return value
351
+ }
352
+
353
+ async _last(name) {
354
+ const tmp = name !== this.name
355
+ const range = PageKey.encodeRange({ gte: [name], lte: [name] })
356
+ const view = tmp ? this.tmp : (this._view ?? this.bee.view)
357
+
358
+ const entry = await view.peek({ ...range, reverse: true })
359
+
360
+ if (entry) {
361
+ const [_, index] = PageKey.decode(entry.key)
362
+
363
+ return index
364
+ }
365
+
366
+ return -1
367
+ }
368
+
369
+ async _tryPut(name, index, value) {
370
+ const tmp = name !== this.name
371
+ const key = PageKey.encode([name, index])
372
+ const view = tmp ? this.tmp : (this._view ?? this.bee.view)
373
+ const batch = this._files.get(name) ?? view.write()
374
+
375
+ this._files.set(name, batch)
376
+
377
+ if (this.compressed) {
378
+ value = await deflate(value, { level: this._compressionLevel })
379
+ }
380
+
381
+ if (tmp && this.encrypted) {
382
+ value = this._encryption.encrypt(value, name, index)
383
+ }
384
+
385
+ batch.tryPut(key, value)
386
+ }
387
+
388
+ async _tryDelete(name, index) {
389
+ const tmp = name !== this.name
390
+ const key = PageKey.encode([name, index])
391
+ const view = tmp ? this.tmp : (this._view ?? this.bee.view)
392
+ const batch = this._files.get(name) ?? view.write()
393
+
394
+ this._files.set(name, batch)
395
+
396
+ batch.tryDelete(key)
397
+ }
398
+
399
+ async _flush(name) {
400
+ const tmp = name !== this.name
401
+ const batch = this._files.get(name)
402
+
403
+ if (!batch) throw errors.INTERNAL(`No batch for ${name}`)
404
+
405
+ this._files.delete(name)
406
+
407
+ await batch.flush()
408
+
409
+ if (tmp) batch.destroy()
410
+ }
411
+
412
+ async _xDelete(name, callback) {
413
+ try {
414
+ const index = await this._last(name)
415
+
416
+ if (index >= 0) {
417
+ for (let i = 0; i <= index; i++) {
418
+ await this._tryDelete(name, i)
419
+ }
420
+ }
421
+
422
+ await this._flush(name)
423
+
424
+ callback(null)
425
+ } catch (err) {
426
+ callback(err)
427
+ }
428
+ }
429
+
430
+ async _xAccess(name, callback) {
431
+ try {
432
+ const index = await this._last(name)
433
+
434
+ callback(null, index >= 0)
435
+ } catch (err) {
436
+ callback(err)
437
+ }
438
+ }
439
+
440
+ async _xRead(name, buffer, offset, callback) {
441
+ try {
442
+ const data = Buffer.from(buffer)
443
+ const end = offset + data.byteLength
444
+ const startPage = Math.floor(offset / PAGE_SIZE)
445
+ const endPage = Math.floor((end - 1) / PAGE_SIZE)
446
+
447
+ for (let i = startPage; i <= endPage; i++) {
448
+ const pageStart = i * PAGE_SIZE
449
+ const pageEnd = pageStart + PAGE_SIZE
450
+ const readStart = Math.max(offset, pageStart)
451
+ const readEnd = Math.min(end, pageEnd)
452
+
453
+ if (readStart < readEnd) {
454
+ const page = (await this._get(name, i)) ?? Buffer.alloc(PAGE_SIZE)
455
+ const sliceStart = readStart - pageStart
456
+ const sliceLength = readEnd - readStart
457
+ const dataOffset = readStart - offset
458
+
459
+ page.copy(data, dataOffset, sliceStart, sliceStart + sliceLength)
460
+ }
461
+ }
462
+
463
+ callback(null)
464
+ } catch (err) {
465
+ callback(err)
466
+ }
467
+ }
468
+
469
+ async _xWrite(name, buffer, offset, callback) {
470
+ try {
471
+ const data = Buffer.from(buffer)
472
+ const start = Math.floor(offset / PAGE_SIZE)
473
+ const end = Math.floor((offset + data.byteLength - 1) / PAGE_SIZE)
474
+
475
+ for (let i = start; i <= end; i++) {
476
+ const pageStart = i * PAGE_SIZE
477
+ const pageEnd = pageStart + PAGE_SIZE
478
+ const writeStart = Math.max(offset, pageStart)
479
+ const writeEnd = Math.min(offset + data.byteLength, pageEnd)
480
+ const writeLength = writeEnd - writeStart
481
+ const pageOffset = writeStart - pageStart
482
+ const dataStart = writeStart - offset
483
+
484
+ if (writeLength === PAGE_SIZE) {
485
+ await this._tryPut(name, i, data.subarray(dataStart, dataStart + PAGE_SIZE))
486
+ } else {
487
+ const page = (await this._get(name, i)) ?? Buffer.alloc(PAGE_SIZE)
488
+ const patch = data.subarray(dataStart, dataStart + writeLength)
489
+
490
+ patch.copy(page, pageOffset)
491
+ await this._tryPut(name, i, page)
492
+ }
493
+ }
494
+
495
+ callback(null)
496
+ } catch (err) {
497
+ callback(err)
498
+ }
499
+ }
500
+
501
+ async _xTruncate(name, size, callback) {
502
+ try {
503
+ const index = await this._last(name)
504
+ const current = (index + 1) * PAGE_SIZE
505
+
506
+ if (size > current) {
507
+ const targetIndex = Math.floor((size - 1) / PAGE_SIZE)
508
+
509
+ for (let i = index + 1; i <= targetIndex; i++) {
510
+ await this._tryPut(name, i, Buffer.alloc(PAGE_SIZE))
511
+ }
512
+ } else if (size < current) {
513
+ const start = Math.floor(size / PAGE_SIZE)
514
+ const remainder = size % PAGE_SIZE
515
+
516
+ for (let i = lastIndex; i > start; i--) {
517
+ await this._tryDelete(name, i)
518
+ }
519
+
520
+ if (remainder > 0) {
521
+ const page = await this._get(name, start)
522
+ const data = Buffer.alloc(PAGE_SIZE)
523
+
524
+ if (page) {
525
+ const length = Math.min(page.byteLength, remainder)
526
+ page.value.copy(data, 0, 0, length)
527
+ }
528
+
529
+ await this._tryPut(name, start, data)
530
+ }
531
+ }
532
+
533
+ callback(null)
534
+ } catch (err) {
535
+ callback(err)
536
+ }
537
+ }
538
+
539
+ async _xSync(name, callback) {
540
+ try {
541
+ await this._flush(name)
542
+ callback(null)
543
+ } catch (err) {
544
+ callback(err)
545
+ }
546
+ }
547
+
548
+ async _xSize(name, callback) {
549
+ try {
550
+ const index = await this._last(name)
551
+ const size = (index + 1) * PAGE_SIZE
552
+
553
+ callback(null, size)
554
+ } catch (err) {
555
+ callback(err)
556
+ }
557
+ }
558
+ }
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "@bullet./paraql",
3
+ "version": "0.1.0",
4
+ "description": "Parallel multi-writer relational database with SQL syntax. Native support for encryption, compression, and vector search.",
5
+ "author": {
6
+ "name": "Tomas Ravinskas",
7
+ "email": "tomas@tomasrav.me",
8
+ "url": "https://tomasrav.me/"
9
+ },
10
+ "license": "Apache-2.0",
11
+ "addon": true,
12
+ "scripts": {
13
+ "test": "npm run test:bare && npm run test:node",
14
+ "test:bare": "brittle-bare test/all.js",
15
+ "test:node": "brittle-node test/all.js",
16
+ "test:manual": "bare test/manual.js",
17
+ "bench": "brittle-bare test/bench.js"
18
+ },
19
+ "files": [
20
+ "index.js",
21
+ "binding.c",
22
+ "binding.js",
23
+ "CMakeLists.txt",
24
+ "NOTICE",
25
+ "cmake",
26
+ "lib",
27
+ "prebuilds"
28
+ ],
29
+ "exports": {
30
+ "./package": "./package.json",
31
+ ".": {
32
+ "types": "./index.d.ts",
33
+ "default": "./index.js"
34
+ }
35
+ },
36
+ "imports": {
37
+ "buffer": {
38
+ "bare": "bare-buffer",
39
+ "default": "buffer"
40
+ },
41
+ "fs": {
42
+ "bare": "bare-fs",
43
+ "default": "fs"
44
+ },
45
+ "fs/promises": {
46
+ "bare": "bare-fs/promises",
47
+ "default": "fs/promises"
48
+ },
49
+ "path": {
50
+ "bare": "bare-path",
51
+ "default": "path"
52
+ },
53
+ "zlib": {
54
+ "bare": "bare-zlib",
55
+ "default": "zlib"
56
+ }
57
+ },
58
+ "homepage": "https://github.com/getbullet-app/paraql",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/getbullet-app/paraql.git"
62
+ },
63
+ "funding": [
64
+ {
65
+ "type": "github",
66
+ "url": "https://github.com/sponsors/OzymandiasTheGreat"
67
+ }
68
+ ],
69
+ "keywords": [
70
+ "sql",
71
+ "autobase",
72
+ "vector-search"
73
+ ],
74
+ "dependencies": {
75
+ "autobee": "^1.0.10",
76
+ "bare-buffer": "^3.6.2",
77
+ "bare-fs": "^4.7.4",
78
+ "bare-path": "^3.1.1",
79
+ "bare-zlib": "^1.4.1",
80
+ "compact-encoding": "^3.3.0",
81
+ "index-encoder": "^3.5.0",
82
+ "ready-resource": "^1.2.0",
83
+ "require-addon": "^1.2.0",
84
+ "rocksdb-native": "^3.17.2",
85
+ "sodium-native": "^5.1.0"
86
+ },
87
+ "devDependencies": {
88
+ "bare-compat-napi": "^1.3.9",
89
+ "brittle": "^4.0.2",
90
+ "cmake-bare": "^1.8.0",
91
+ "cmake-fetch": "^1.5.2",
92
+ "cmake-napi": "^1.2.2",
93
+ "cmake-npm": "^1.1.2",
94
+ "corestore": "^7.11.0",
95
+ "prettier": "^3.9.4",
96
+ "test-tmp": "^1.4.0"
97
+ }
98
+ }