@reddb-io/sdk 1.20.0-rc.178 → 1.20.0-rc.180

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.
Files changed (2) hide show
  1. package/README.md +123 -6
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -164,6 +164,71 @@ try {
164
164
  directly via `tx.query()`. Open another `connect()` handle for independent
165
165
  concurrent transactions.
166
166
 
167
+ ### Isolation levels
168
+
169
+ The default isolation is **snapshot isolation** (each transaction reads a
170
+ consistent MVCC snapshot). To request a stronger level, issue the isolation
171
+ clause with the opening statement via `db.query()`:
172
+
173
+ ```js
174
+ await db.query('BEGIN ISOLATION LEVEL SERIALIZABLE')
175
+ await db.query("INSERT INTO audit (action) VALUES ('serializable write')")
176
+ await db.query('COMMIT')
177
+ ```
178
+
179
+ RedDB accepts the PG-compatible spellings `READ UNCOMMITTED`,
180
+ `READ COMMITTED`, `REPEATABLE READ` (= `SNAPSHOT`), and `SERIALIZABLE`.
181
+ `SERIALIZABLE` engages the serializable-snapshot-isolation (SSI) path, which
182
+ can abort a transaction at commit with a serialization conflict (see below).
183
+
184
+ ### Serialization conflicts & retries
185
+
186
+ Under snapshot / serializable isolation, two transactions that write the same
187
+ row on overlapping snapshots resolve **first-committer-wins**: the later
188
+ transaction is aborted with a **retryable serialization conflict**. It
189
+ surfaces as a `RedDBError` with code `QUERY_ERROR` whose message begins
190
+ `serialization conflict`. This is the one error class you should catch and
191
+ **retry** rather than surface to the user:
192
+
193
+ ```js
194
+ import { connect, RedDBError } from '@reddb-io/sdk'
195
+
196
+ const db = await connect('file:///var/lib/reddb/data.rdb')
197
+
198
+ /** True only for the retryable first-committer-wins conflict. */
199
+ function isSerializationConflict(err) {
200
+ return (
201
+ err instanceof RedDBError &&
202
+ err.code === 'QUERY_ERROR' &&
203
+ /serialization conflict/i.test(err.message)
204
+ )
205
+ }
206
+
207
+ /** Run a transaction, retrying with backoff on serialization conflicts. */
208
+ async function withRetry(fn, { maxRetries = 5 } = {}) {
209
+ for (let attempt = 0; ; attempt++) {
210
+ try {
211
+ return await db.transaction(fn)
212
+ } catch (err) {
213
+ if (isSerializationConflict(err) && attempt < maxRetries) {
214
+ await new Promise((r) => setTimeout(r, 2 ** attempt * 5)) // backoff
215
+ continue
216
+ }
217
+ throw err // not retryable, or out of attempts
218
+ }
219
+ }
220
+ }
221
+
222
+ // Concurrent debits stay consistent — losers retry against the fresh snapshot.
223
+ await withRetry(async (tx) => {
224
+ const { rows } = await tx.query('SELECT balance FROM accounts WHERE id = $1', 1)
225
+ await tx.query('UPDATE accounts SET balance = $1 WHERE id = $2', rows[0].balance - 10, 1)
226
+ })
227
+ ```
228
+
229
+ Only conflicts are retried — a syntax error, a constraint violation, or any
230
+ other `QUERY_ERROR` propagates on the first attempt.
231
+
167
232
  ## Connection URIs
168
233
 
169
234
  | URI | Mode |
@@ -299,6 +364,54 @@ await db.queues.len('jobs')
299
364
  await db.queues.purge('jobs')
300
365
  ```
301
366
 
367
+ ## Graph, vector and time-series
368
+
369
+ RedDB is one multi-model engine: graphs, vectors and time-series live in the
370
+ same database and are reached through `db.query()` (dedicated helpers arrive in
371
+ Helper Spec v1.1). Every snippet below is executed on each release by
372
+ [`test/readme-examples.test.mjs`](test/readme-examples.test.mjs).
373
+
374
+ **Graph** — nodes and edges in a `network` collection, then a shortest-path
375
+ traversal (the first user row gets `rid` 1024):
376
+
377
+ ```js
378
+ await db.query("INSERT INTO network NODE (label, node_type) VALUES ('gateway', 'Host')")
379
+ await db.query("INSERT INTO network NODE (label, node_type) VALUES ('app', 'Host')")
380
+ await db.query("INSERT INTO network NODE (label, node_type) VALUES ('db', 'Host')")
381
+ await db.query("INSERT INTO network EDGE (label, from, to, weight) VALUES ('connects', 1024, 1025, 1.0)")
382
+ await db.query("INSERT INTO network EDGE (label, from, to, weight) VALUES ('connects', 1025, 1026, 1.0)")
383
+ await db.query("INSERT INTO network EDGE (label, from, to, weight) VALUES ('connects', 1024, 1026, 5.0)")
384
+
385
+ const path = await db.query("GRAPH SHORTEST_PATH '1024' TO '1026' ALGORITHM dijkstra")
386
+ path.rows[0].total_weight // 2 — two 1.0 hops beat the single 5.0 edge
387
+ ```
388
+
389
+ **Vector** — store embeddings, then rank by similarity. A vector bind value is
390
+ a single `$N` param, so it goes in the params array as one element
391
+ (`[[1.0, 0.0]]`), not the variadic form:
392
+
393
+ ```js
394
+ await db.query("INSERT INTO embeddings VECTOR (dense, content) VALUES ([1.0, 0.0], 'gateway runbook')")
395
+ await db.query("INSERT INTO embeddings VECTOR (dense, content) VALUES ([0.0, 1.0], 'database manual')")
396
+
397
+ const hits = await db.query('SEARCH SIMILAR $1 COLLECTION embeddings LIMIT 1', [[1.0, 0.0]])
398
+ hits.rows[0].score // 1 — the identical vector scores exactly 1
399
+ ```
400
+
401
+ **Time-series** — declare a series with retention + downsampling, ingest
402
+ points (timestamps are nanoseconds), then bucket them:
403
+
404
+ ```js
405
+ await db.query('CREATE TIMESERIES metrics RETENTION 7 d CHUNK_SIZE 64 DOWNSAMPLE 1h:5m:avg')
406
+ await db.query("INSERT INTO metrics (metric, value, tags, timestamp) VALUES ('cpu.usage', 10.0, '{\"host\":\"srv-a\"}', 0)")
407
+ await db.query("INSERT INTO metrics (metric, value, tags, timestamp) VALUES ('cpu.usage', 20.0, '{\"host\":\"srv-a\"}', 60000000000)")
408
+
409
+ const rollup = await db.query(
410
+ 'SELECT time_bucket(5m) AS bucket, avg(value) AS avg_value, count(*) AS samples ' +
411
+ "FROM metrics WHERE metric = 'cpu.usage' GROUP BY time_bucket(5m)",
412
+ )
413
+ ```
414
+
302
415
  ## SDK Helper Spec conformance
303
416
 
304
417
  This driver implements **SDK Helper Spec v1.0**
@@ -354,12 +467,16 @@ with `INVALID_ARGUMENT`.
354
467
  | `wire.graph.sql_round_trip` | reachable via `db.query` (no v1.0 case) |
355
468
  | `wire.timeseries.sql_round_trip` | reachable via `db.query` (no v1.0 case) |
356
469
 
357
- **Out-of-scope in v1.0** (reach via raw `db.query` until v1.1, per spec):
358
- first-class `vectors.*`, `graph.*`, `timeseries.*`, and `probabilistic.*`
359
- helpers; KV TTL (`kv.expire`) and gRPC watch; priority queues, consumer
360
- groups, dead-letter routing; transaction isolation-level arguments and
361
- cross-shard transactions; JSON Patch / nested / array-positional document
362
- patches (top-level merge only).
470
+ **Out-of-scope in v1.0 helpers** (reach via raw `db.query` until v1.1, per
471
+ spec): first-class `vectors.*`, `graph.*`, `timeseries.*`, and
472
+ `probabilistic.*` helpers (see [Graph, vector and
473
+ time-series](#graph-vector-and-time-series)); KV TTL (`kv.expire`) and gRPC
474
+ watch; priority queues, consumer groups, dead-letter routing; cross-shard
475
+ transactions; JSON Patch / nested / array-positional document patches
476
+ (top-level merge only). Transaction **isolation levels** and the retryable
477
+ serialization-conflict class are available today through raw `db.query` — see
478
+ [Isolation levels](#isolation-levels) and [Serialization conflicts &
479
+ retries](#serialization-conflicts--retries).
363
480
 
364
481
  Run the conformance harness against a locally built binary:
365
482
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/sdk",
3
- "version": "1.20.0-rc.178",
3
+ "version": "1.20.0-rc.180",
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",