@reddb-io/cli 1.2.5 → 1.3.1

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
@@ -635,3 +635,30 @@ RedDB from another Rust project.
635
635
  ---
636
636
 
637
637
  **AGPL-3.0 License** -- Built by [RedDB.io](https://github.com/reddb-io)
638
+
639
+ <!-- contract-matrix:begin -->
640
+ ## Public-surface support
641
+
642
+ > Generated from [`docs/conformance/public-surface-contract-matrix.json`](/docs/conformance/public-surface-contract-matrix.json) by `scripts/gen-docs-from-matrix.mjs`. Do not edit between the markers by hand — run `node scripts/gen-docs-from-matrix.mjs --write`. The matrix is the source of truth; this block can never claim more than it, and CI (`docs-matrix`) fails on drift.
643
+ >
644
+ > Every public RedDB promise and the status of each public surface that offers it.
645
+
646
+ | Promise | sql | http | redwire | grpc | driver_helpers |
647
+ | --- | --- | --- | --- | --- | --- |
648
+ | **PSC-001** — RedDB is one multi-model database (tables, graph, KV, timeseries, probabilistic, vector, queue, documents) backed by a single file. | ✅ supported | ✅ supported | ✅ supported | ✅ supported | ✅ supported |
649
+ | **PSC-002** — MATCH supports node, edge, label, property, and LIMIT projections. | ✅ supported | ✅ supported | ⚠️ partial | ⚠️ partial | ✅ supported |
650
+ | **PSC-003** — GRAPH algorithms accept semantic identifiers, limits, ordering, and return stable rich rows. | ✅ supported | ✅ supported | ❌ unsupported | ❌ unsupported | ❌ unsupported |
651
+ | **PSC-004** — INSERT creates rows, documents, and native timeseries points. | ✅ supported | ✅ supported | ⚠️ partial | ✅ supported | ✅ supported |
652
+ | **PSC-005** — HLL/SKETCH/FILTER expose write and read commands for cardinality, frequency, and membership. | ⚠️ partial | ❌ unsupported | ❌ unsupported | ❌ unsupported | ⚠️ partial |
653
+ | **PSC-006** — Timeseries stores timestamped metrics with tags and supports query/readback. | ✅ supported | ⚠️ partial | ❌ unsupported | ❌ unsupported | ⚠️ partial |
654
+ | **PSC-007** — Documents are first-class: create, read, update, delete, and SQL analytics over JSON. | ✅ supported | ✅ supported | ❌ unsupported | ✅ supported | ✅ supported |
655
+ | **PSC-008** — KV helpers expose get/put/delete; get of a missing key returns null, delete reports affected. | ✅ supported | ❌ unsupported | ❌ unsupported | ✅ supported | ✅ supported |
656
+ | **PSC-009** — Queue helpers expose create/push/peek/pop/len/purge with FIFO semantics; empty pop is not an error. | ✅ supported | ❌ unsupported | ❌ unsupported | ❌ unsupported | ✅ supported |
657
+ | **PSC-010** — Transactions are imperative (begin/commit/rollback) plus a run(callback) form; empty SQL rejects with INVALID_ARGUMENT. | ✅ supported | ❌ unsupported | ❌ unsupported | ✅ supported | ✅ supported |
658
+ | **PSC-011** — SQL aggregate, projection, expression, and mutation behaviour matches ordinary SQL expectations where advertised. | ✅ supported | ✅ supported | ⚠️ partial | ⚠️ partial | ✅ supported |
659
+ | **PSC-012** — Server transports expose the same query contract as embedded (HTTP, RedWire, gRPC parity). | ✅ supported | ✅ supported | ✅ supported | ✅ supported | ✅ supported |
660
+ | **PSC-013** — Official drivers implement the SDK Helper Spec v1.0 conformance suite (all 22 §12 case IDs). | ❌ unsupported | ❌ unsupported | ❌ unsupported | ✅ supported | ✅ supported |
661
+ | **PSC-014** — ASK / SEARCH semantic surfaces return ranked results with stable shape. | ✅ supported | ⚠️ partial | ❌ unsupported | ❌ unsupported | ⚠️ partial |
662
+
663
+ _Status legend: ✅ supported · ⚠️ partial (known gaps) · ❌ unsupported._
664
+ <!-- contract-matrix:end -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/sdk",
3
- "version": "1.2.5",
3
+ "version": "1.3.1",
4
4
  "description": "Official embedded RedDB SDK — launches a local red binary over stdio JSON-RPC. Use @reddb-io/client for remote HTTP, gRPC, and RedWire.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "postinstall": "node postinstall.js",
23
- "test": "node --test test/ask.test.mjs test/cache.test.mjs test/db-helpers.test.mjs test/embedded-only.test.mjs test/insert-ids.test.mjs test/kv.test.mjs test/params.test.mjs test/postinstall.test.mjs test/queue.test.mjs test/redwire.params.test.mjs test/transaction.test.mjs && node test/smoke.test.mjs"
23
+ "test": "node --test test/ask.test.mjs test/cache.test.mjs test/db-helpers.test.mjs test/embedded-only.test.mjs test/helpers.test.mjs test/insert-ids.test.mjs test/kv.test.mjs test/params.test.mjs test/postinstall.test.mjs test/queue.test.mjs test/redwire.params.test.mjs test/transaction.test.mjs && node test/smoke.test.mjs && node test/conformance.test.mjs && node test/readme-examples.test.mjs"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -50,7 +50,7 @@ export class TypedQueryBuilder {
50
50
  ? '*'
51
51
  : this.columns.map(sqlIdentifierPath).join(', ')
52
52
  const where = this.whereClauses.length > 0
53
- ? ` WHERE ${this.whereClauses.join(' AND ')}`
53
+ ? ` WHERE ${this.whereClauses.map((clause) => `(${clause})`).join(' AND ')}`
54
54
  : ''
55
55
  const sql = `SELECT ${projection} FROM ${sqlIdentifierPath(this.collection)}${where}`
56
56
  const result = this.params.length > 0
@@ -40,7 +40,10 @@ export class DocumentClient {
40
40
  validateObject(patch, 'documents.patch patch')
41
41
  const entries = Object.entries(patch)
42
42
  if (entries.length === 0) {
43
- return this.get(collection, rid)
43
+ throw new RedDBError(
44
+ 'INVALID_ARGUMENT',
45
+ 'documents.patch patch must be a non-empty object',
46
+ )
44
47
  }
45
48
  for (const [field] of entries) {
46
49
  if (field.includes('/')) {
@@ -66,7 +69,8 @@ export class DocumentClient {
66
69
 
67
70
  async delete(collection, rid) {
68
71
  const result = await this.db.delete(collection, rid)
69
- return { affected: result.affected ?? 0 }
72
+ const affected = result.affected ?? 0
73
+ return { affected, deleted: affected > 0 }
70
74
  }
71
75
 
72
76
  async ensureCollection(collection) {
@@ -43,6 +43,13 @@ export { parseUri, deriveLoginUrl } from './url.js'
43
43
  export const EMBEDDED_ONLY_MESSAGE =
44
44
  'remote URIs are not supported in @reddb-io/sdk; install @reddb-io/client for grpc/http/red transports'
45
45
 
46
+ /**
47
+ * SDK Helper Spec version this driver implements. See
48
+ * `docs/spec/sdk-helpers.md` §14 — every official driver exposes this so
49
+ * cross-driver CI dashboards can assert against it.
50
+ */
51
+ export const HELPER_SPEC_VERSION = '1.0'
52
+
46
53
  const MIN_INSERT_ID_ENGINE_VERSION = '1.0.9'
47
54
  const NESTED_TX_NOT_SUPPORTED = 'NESTED_TX_NOT_SUPPORTED'
48
55
 
@@ -304,6 +311,92 @@ class TransactionHandle {
304
311
  }
305
312
  }
306
313
 
314
+ /**
315
+ * Spec §7 transaction client. Returned by `db.tx()`. Exposes the imperative
316
+ * `begin` / `commit` / `rollback` trio (each resolves to a `QueryResult`) plus
317
+ * the optional `run(callback)` form. Transaction state is tracked on the parent
318
+ * `RedDB` so it serialises with `db.transaction()` and nested opens are
319
+ * rejected rather than silently interleaved.
320
+ */
321
+ export class TxClient {
322
+ constructor(db) {
323
+ this.db = db
324
+ this.active = false
325
+ }
326
+
327
+ async begin() {
328
+ if (this.db.inTransaction) {
329
+ throw nestedTransactionError()
330
+ }
331
+ this.db.inTransaction = true
332
+ this.active = true
333
+ try {
334
+ return await this.db.query('BEGIN')
335
+ } catch (err) {
336
+ this.db.inTransaction = false
337
+ this.active = false
338
+ throw err
339
+ }
340
+ }
341
+
342
+ async commit() {
343
+ if (!this.active) {
344
+ throw new RedDBError('INVALID_ARGUMENT', 'tx.commit() called without an open transaction')
345
+ }
346
+ try {
347
+ return await this.db.query('COMMIT')
348
+ } finally {
349
+ this.active = false
350
+ this.db.inTransaction = false
351
+ }
352
+ }
353
+
354
+ async rollback() {
355
+ if (!this.active) {
356
+ throw new RedDBError('INVALID_ARGUMENT', 'tx.rollback() called without an open transaction')
357
+ }
358
+ try {
359
+ return await this.db.query('ROLLBACK')
360
+ } finally {
361
+ this.active = false
362
+ this.db.inTransaction = false
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Callback form: commit on success, roll back and re-throw on failure.
368
+ * Nested `tx.run` rejects with `INVALID_ARGUMENT` — callers wanting
369
+ * savepoints issue them directly via `tx.query()` (spec §7.2; the README
370
+ * records this choice).
371
+ */
372
+ async run(callback) {
373
+ if (typeof callback !== 'function') {
374
+ throw new TypeError('tx.run(callback) requires a function')
375
+ }
376
+ if (this.db.inTransaction) {
377
+ throw new RedDBError(
378
+ 'INVALID_ARGUMENT',
379
+ 'nested tx.run() is not supported; issue savepoints via tx.query() instead',
380
+ )
381
+ }
382
+ await this.begin()
383
+ try {
384
+ const result = await callback(new TransactionHandle(this.db))
385
+ await this.commit()
386
+ return result
387
+ } catch (err) {
388
+ if (this.active) {
389
+ try {
390
+ await this.rollback()
391
+ } catch (rollbackErr) {
392
+ attachRollbackError(err, rollbackErr)
393
+ }
394
+ }
395
+ throw err
396
+ }
397
+ }
398
+ }
399
+
307
400
  export class RedDB {
308
401
  /**
309
402
  * @param {RpcClient} client
@@ -315,8 +408,12 @@ export class RedDB {
315
408
  constructor(client, opts = {}) {
316
409
  this.client = client
317
410
  this.transport = opts.transport ?? null
411
+ this.helperSpecVersion = HELPER_SPEC_VERSION
318
412
  this.cache = new CacheClient(client, this.transport)
319
413
  this.queue = new QueueClient(client)
414
+ // Spec §6: the canonical namespace is the plural `queues`. `queue` is kept
415
+ // as a back-compat alias to the same handle.
416
+ this.queues = this.queue
320
417
  this.documents = new DocumentClient(this)
321
418
  const defaultKv = new KvClient(client)
322
419
  this.kv = Object.assign((collection = 'kv_default') => new KvClient(client, collection), {
@@ -341,6 +438,13 @@ export class RedDB {
341
438
  * Returns `{ statement, affected, columns, rows }`.
342
439
  */
343
440
  query(sql, ...params) {
441
+ // Spec §3.1 / §2.5: empty SQL is a caller bug; reject locally before
442
+ // touching the wire.
443
+ if (typeof sql !== 'string' || sql.trim().length === 0) {
444
+ return Promise.reject(
445
+ new RedDBError('INVALID_ARGUMENT', 'query() requires a non-empty SQL string'),
446
+ )
447
+ }
344
448
  const wireParams = normalizeQueryParams(params)
345
449
  if (wireParams == null) {
346
450
  return this.client.call('query', { sql }).then(normalizeResult)
@@ -361,10 +465,23 @@ export class RedDB {
361
465
 
362
466
  /** Insert many rows in one call. Returns `{ affected, rids, ids }`; `ids` is a legacy alias. */
363
467
  async bulkInsert(collection, payloads) {
468
+ // Spec §3.4: empty payloads is a no-op returning `{ affected: 0, rids: [] }`.
469
+ if (Array.isArray(payloads) && payloads.length === 0) {
470
+ return { affected: 0, rids: [], ids: [] }
471
+ }
364
472
  const result = await this.client.call('bulk_insert', { collection, payloads })
365
473
  return requireInsertIds(result, payloads.length)
366
474
  }
367
475
 
476
+ /**
477
+ * Spec §7 transaction handle. `db.tx()` returns a {@link TxClient} exposing
478
+ * imperative `begin` / `commit` / `rollback` plus a `run(callback)` form.
479
+ * `db.transaction(callback)` remains as the original callback-only shortcut.
480
+ */
481
+ tx() {
482
+ return new TxClient(this)
483
+ }
484
+
368
485
  async transaction(callback) {
369
486
  if (this.inTransaction) {
370
487
  throw nestedTransactionError()
@@ -17,6 +17,11 @@ export class KvClient {
17
17
  })
18
18
  }
19
19
 
20
+ // Spec-canonical alias for `put` (SDK Helper Spec §5.1 `kv.set`).
21
+ set(key, value, options = {}) {
22
+ return this.put(key, value, options)
23
+ }
24
+
20
25
  async get(key, options = {}) {
21
26
  const collection = options.collection ?? this.collection
22
27
  const result = await this.client.call('query', {
@@ -40,7 +45,9 @@ export class KvClient {
40
45
  const result = await this.client.call('query', {
41
46
  sql: `KV DELETE ${kvPath(collection, key)}`,
42
47
  })
43
- return { affected: result.affected ?? result.affected_rows ?? 0 }
48
+ const affected = result.affected ?? result.affected_rows ?? 0
49
+ // Spec §5.4 / §2.4 DeleteResult: `deleted` is `affected > 0`.
50
+ return { affected, deleted: affected > 0 }
44
51
  }
45
52
 
46
53
  async list(options = {}) {
@@ -5,6 +5,14 @@ export class QueueClient {
5
5
  this.client = client
6
6
  }
7
7
 
8
+ // Spec §6.1 `queues.create`: idempotent (CREATE QUEUE IF NOT EXISTS) so
9
+ // conformance fixtures can prime a queue the same way the Rust/Go harnesses do.
10
+ create(queue) {
11
+ return this.client.call('query', {
12
+ sql: `CREATE QUEUE IF NOT EXISTS ${queueIdentifier(queue)}`,
13
+ })
14
+ }
15
+
8
16
  push(queue, value, options = {}) {
9
17
  const priority = options.priority != null ? ` PRIORITY ${queuePriority(options.priority)}` : ''
10
18
  return this.client.call('query', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/cli",
3
- "version": "1.2.5",
3
+ "version": "1.3.1",
4
4
  "description": "CLI launcher for RedDB. The JS/TS app driver is published as @reddb-io/sdk.",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -37,6 +37,9 @@
37
37
  "scripts": {
38
38
  "postinstall": "node drivers/js/cli-postinstall.js",
39
39
  "test": "node drivers/js/test/smoke.test.mjs",
40
+ "typecheck": "cargo check --workspace --locked",
41
+ "lint": "cargo fmt --all -- --check && cargo clippy --workspace --locked -- -D warnings",
42
+ "build": "cargo test --workspace --locked",
40
43
  "changeset": "changeset",
41
44
  "release:version": "changeset version && node scripts/sync-version.js",
42
45
  "release:publish": "changeset publish",