Skip to content

Commit 1b094b3

Browse files
docs: update for transactions, distributed execution, and protocol changes (#174)
Covers ~490 commits since the last docs update: - query-language: read-your-own-writes staging overlay, savepoint semantics and error codes, computed GROUP BY keys, scalar-aggregate identity row, trailing ENGINE = suffix, RESTORE TENANT FORCE, ANALYZE, version()/current_setting(), geometry constructors in INSERT, memory-budget scan bound - architecture: ANALYZE-driven distributed joins/GROUP BY shuffle, grace-hash spill knobs, streaming SELECT, placement reconcile and log compaction, login rate limits count failures only - protocols: pgwire driver-compat surface, native savepoints, HTTP stream error semantics, sync CollectionSchema/DeltaReject/idempotent producers and quorum durability - graph: distributed algo invocation, var-len MATCH expansion caps - security/auth: DROP USER ownership reassignment, login rate limiting - bitemporal: audit-query _ts_* columns, reserved-column hiding - databases: structural per-engine database_id isolation - fts: analyzer honored on staged-write paths - spatial: geometry constructors - offline-sync: schema announce, constraint rejects, durability
1 parent 6fe7af5 commit 1b094b3

10 files changed

Lines changed: 164 additions & 16 deletions

File tree

docs/architecture.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ NodeDB uses Multi-Raft — each vShard is its own independent Raft group. Three
134134

135135
Each kind has independent leader election. A sequencer leader failure does not affect data-group leaders.
136136

137+
**Placement and rebalancing.** Each Raft group's voter set is derived deterministically by rendezvous (highest-random-weight) hashing over active nodes, taking the top `min(replication_factor, node_count)` nodes per group. A reconcile loop on the metadata-group leader converges membership toward placement on every tick: it proposes learners for placement nodes not yet in membership, promotes them when caught up, evicts out-of-placement learners, removes leaving voters, and transfers leadership away from placement-excluded leaders (via TimeoutNow). Node joins kick an immediate reconcile. `REBALANCE` triggers it manually.
138+
139+
**Log compaction and snapshots.** Raft log auto-compaction is opt-in (`log_compaction_threshold`; disabled by default) and gated on the durable Data-Plane applied watermark — entries are only truncated once their effects are applied, not merely committed. Lagging followers catch up via per-group snapshots that carry the full Data-Plane state (rows, CRDT state, graph edges, PK→surrogate identity map) using exact clear-then-install semantics; persisted `.snap` files re-install on follower boot.
140+
137141
## Cross-Shard Transactions
138142

139143
Write transactions that touch multiple vShards go through the **Calvin sequencer** rather than two-phase commit. The sequencer Raft group produces a globally-ordered log of transaction batches (epochs, default 20 ms). Within each epoch, the scheduler derives a deterministic lock order and the executor runs all writes concurrently without cross-shard coordination.
@@ -150,6 +154,18 @@ SET cross_shard_txn = 'best_effort_non_atomic';
150154

151155
Single-shard writes bypass the sequencer entirely and go directly through the relevant data-group Raft.
152156

157+
## Distributed Query Execution
158+
159+
Multi-node SELECTs pick their execution strategy from `ANALYZE` statistics — run `ANALYZE <collection>` to collect per-column `row_count`, `distinct_count`, and `avg_value_len` into the catalog (auto-ANALYZE re-runs after ~10% of rows change).
160+
161+
**Joins** — The cost model estimates each side's size from statistics and chooses between **broadcast join** (small side shipped to every node) and **shuffle join** (both sides hash-partitioned across nodes over the QUIC streaming transport). Unanalyzed or empty sides fall back to broadcast. Overrides: `SET nodedb.force_shuffle_join = true`, `SET nodedb.broadcast_threshold_bytes = ...` (default from `[tuning.cluster_transport] broadcast_threshold_bytes`, 8 MiB).
162+
163+
**GROUP BY** — High-cardinality aggregations shuffle groups across nodes instead of gathering all rows at the coordinator. Selection is driven by `distinct_count`; overrides: `SET nodedb.shuffle_agg_threshold`, `nodedb.force_shuffle_agg`, `nodedb.shuffle_agg_num_parts`.
164+
165+
**Spill** — Hash joins and aggregations that exceed memory spill to disk via grace-hash partitioning (recursive re-partitioning, io_uring sequential writers). Knobs in `[tuning.query]`: `groupby_max_groups_in_mem` (default 1M), `aggregate_chunk_size` (default 10K), `max_scan_result_bytes` (default 512 MiB — the memory-budget bound on unbounded SELECTs; exceeding it returns a typed `ResourcesExhausted` error, never a silent truncation).
166+
167+
**Streaming** — Eligible unordered SELECT results stream from Data Plane cores over QUIC to the coordinator and on to the client (pgwire, native, HTTP NDJSON) without materializing the full result set on the coordinator.
168+
153169
## Cross-Engine Queries
154170

155171
All engines share the same snapshot, transaction context, and memory budget. A query that combines vector similarity, graph traversal, spatial filtering, and document field access executes inside one process — no network hops between engines, no application-level joins.
@@ -268,10 +284,11 @@ user:{id} → org:{id} → tenant:{id} → database:{id}
268284
269285
The database bucket capacity is `database.quota.max_qps`. Additionally, pre-auth login rate limits are enforced:
270286
271-
- `login_ip:{addr}` — capacity `cluster.login_attempts_per_ip_per_min` (default 30)
272-
- `login_user:{username}` — capacity `cluster.login_attempts_per_user_per_min` (default 10)
287+
- `login_ip:{addr}` — capacity `cluster.login_attempts_per_ip_per_min` (default 30 **failures**/min)
288+
- `login_user:{username}` — capacity `cluster.login_attempts_per_user_per_min` (default 10 **failures**/min)
289+
- Argon2-DoS ceiling per IP — `max(ip_cap × 4, 120)` credential verifications/min, consumed on every attempt
273290
274-
These pre-auth buckets are consulted before SCRAM/Argon2 verification (cheap exit path) and run in constant time with a uniform delay on any denial to prevent timing leaks.
291+
Only genuine credential failures consume the failure budgets — the admission check peeks the buckets, so a burst of successful reconnects (e.g., a warming connection pool) is never rejected. These pre-auth buckets are consulted before SCRAM/Argon2 verification (cheap exit path) and run in constant time with a uniform delay on any denial to prevent timing leaks. A rate-limit denial returns a distinct, retryable `TOO_MANY_CONNECTIONS` error rather than a generic credential failure.
275292
276293
### Per-Tenant Compute Caps
277294

docs/bitemporal.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ AS OF SYSTEM TIME NULL -- returns every system-time version, ascending
162162
ORDER BY _ts_system ASC;
163163
```
164164

165-
`AS OF SYSTEM TIME NULL` returns every system-time version of each matching row, ordered ascending, with the `_ts_system` column projected into the result. It is supported on the Document (strict and schemaless), Columnar, and Timeseries engines. It is not supported on the Graph or Array engines, nor when reading through a database clone — those return a typed error rather than collapsing to a single version.
165+
`AS OF SYSTEM TIME NULL` returns every system-time version of each matching row, ordered ascending. Each version row carries the user columns plus three synthetic temporal columns: `_ts_system`, `_ts_valid_from`, and `_ts_valid_until`. Unbounded valid-time ends surface as the raw `i64::MIN` / `i64::MAX` sentinels (matching the Columnar and Timeseries engines). It is supported on the Document (strict and schemaless), Columnar, and Timeseries engines. It is not supported on the Graph or Array engines, nor when reading through a database clone — those return a typed error rather than collapsing to a single version.
166+
167+
**Reserved columns never leak.** Bitemporal collections store their temporal envelope in reserved columns (`__system_from_ms`, `__valid_from_ms`, `__valid_until_ms`). These are hidden from user projections: `SELECT *` on a bitemporal collection returns exactly the declared user columns. Temporal data is only exposed through the synthetic `_ts_*` columns of audit queries.
166168

167169
## Index Engines and Temporal Composition
168170

docs/databases.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ Accessing a collection in a different database than your bound database returns
100100

101101
The only exception: privileged admin DDL (CLONE, MIRROR, MOVE TENANT) can reference multiple databases explicitly.
102102

103+
Isolation is structural, not just a permission check: `database_id` is the outermost key component in every engine's storage — KV, document, graph (CSR and edge store), vector, sparse-vector, spatial, timeseries, columnar, and full-text indexes — as well as the surrogate identity catalog, WAL replication entries, and per-database features like retention policies, continuous aggregates, and alerts. Two databases can hold same-named collections with zero key-space overlap in any engine.
104+
103105
## Quotas
104106

105107
Quotas form a three-tier hierarchy: global (cluster-wide), database (per database), and tenant (per tenant within a database). Each tier has its own budget for memory, storage, queries-per-second, and connections. A request is admitted only if it passes all three tiers.

docs/full-text-search.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ NodeDB's full-text search engine provides Block-Max WAND (BMW) optimized BM25 ra
2020
- **Korean Hangul decomposition** — Decomposes Hangul syllables into Jamo components for morphological matching.
2121
- **AND-first with OR fallback** — Tries AND semantics first; if zero results, falls back to OR with a coverage penalty (`matched_terms / total_terms`).
2222
- **Phrase proximity boost** — Consecutive query tokens at consecutive positions get up to 3x score boost.
23-
- **Per-collection analyzer binding** — Each collection can configure its analyzer and language. Applied consistently at both index time and query time, eliminating mismatch bugs.
23+
- **Per-collection analyzer binding** — Each collection can configure its analyzer and language (`CREATE SEARCH INDEX ... ANALYZER '<name>'` or `ALTER COLLECTION ... SET text_analyzer = '<name>'`). The bound analyzer is honored in every tokenization path — index time, query time, phrase-term canonicalization, and in-transaction staged writes (read-your-own-writes) — eliminating mismatch bugs.
2424
- **Field-aware BM25** — Weighted multi-field scoring: `final_score = Σ(weight_i × bm25(field_i))`. Title matches score higher than body matches.
2525
- **Fuzzy matching** — Levenshtein distance-based matching with adaptive thresholds (1 edit for 4-6 chars, 2 for 7+).
2626
- **Synonyms** — Define synonym groups for query-time expansion.

docs/graph.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ RETURN DISTINCT related.id, related.description;
322322

323323
## Distributed Execution (BSP)
324324

325-
In multi-shard deployments, graph algorithms and pattern matching execute via Bulk Synchronous Parallel (BSP) coordination.
325+
In multi-shard deployments, graph algorithms and pattern matching execute via Bulk Synchronous Parallel (BSP) coordination. There is no separate distributed syntax — the same `GRAPH ALGO` and `MATCH` statements run single-node or distributed transparently; the coordinator picks the BSP pipeline when the graph is partitioned. Distributed PageRank (including `PERSONALIZATION`), WCC, and cross-shard MATCH are supported; results are globally correct (e.g., PageRank sums to ≈1.0 across shards).
326326

327327
### Distributed PageRank
328328

@@ -354,6 +354,19 @@ Scatter-gather with continuations:
354354
5. Coordinator collects completed rows and new continuations
355355
6. Repeat until no pending continuations or max rounds reached
356356

357+
### Variable-Length Expansion Caps
358+
359+
Variable-length `[*min..max]` MATCH expansion is bounded by operational knobs in the `[tuning.graph]` section of `config.toml`:
360+
361+
| Knob | Default | Effect |
362+
| -------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
363+
| `varlen_max_results` | 100000 | Cap on results from one variable-length expansion. On overflow, truncates at the hop boundary and pages via a resume cursor — no row is silently dropped. |
364+
| `varlen_max_frontier`| 100000 | Cap on the live per-hop frontier; overflow pages via resume. |
365+
| `max_depth` | 10 | Maximum traversal depth. |
366+
| `max_visited` | 100000 | Memory budget on visited nodes. |
367+
368+
Resume cursors work both locally and across shards, so a capped expansion continues from where it stopped instead of failing.
369+
357370
---
358371

359372
## Bitemporal Support

docs/offline-sync-patterns.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ LIMIT 1;
115115

116116
Columnar, Vector, FTS, and Spatial collections now participate in **inbound sync** to NodeDB-Lite (alongside the document and CRDT engines). Schema changes (DDL) broadcast to connected Lite and WASM sessions after the Origin catalog commits, so embedded clients automatically pick up new collections and columns without explicit subscription. This enables seamless schema evolution in offline-first applications.
117117

118+
**Schema announce.** A sync peer announces each collection's schema (`CollectionSchema` message) before sending any shape or delta data. Collections unknown to the receiver are materialized into its catalog and propagate cluster-wide via Raft — create-only, so an existing collection is never clobbered. A Lite client can therefore bootstrap collections on Origin simply by announcing their schema.
119+
120+
**Constraint validation and rejects.** UNIQUE, FK, required-field, and CHECK constraints are validated when deltas apply on Origin. Because CRDT deltas are commutative and already merged on import, enforcement is a post-hoc pass: a violating row is quarantined and the client receives a `DeltaReject` carrying a typed `CompensationHint` (e.g., `UniqueViolation { field, conflicting_value }`, `RetryWithDifferentValue`, `ManualIntervention`) telling it precisely what to fix. A rejected delta does not wedge the stream — later deltas continue to apply.
121+
122+
**Durability.** Acknowledged sync writes are quorum-durable: the delta commits through the data group's Raft log before the ack, so it survives leader failover. Producers carry `(producer_id, epoch, seq)` provenance; replays are acknowledged as duplicates rather than double-applied.
123+
118124
## NodeDB-Lite Usage
119125

120126
### Creating Offline Invoices (Swift/Kotlin via FFI)

docs/protocols.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ psql -h localhost -p 6432
2828

2929
**SQL coverage:** Everything in the [query language reference](query-language.md).
3030

31-
**Introspection:** NodeDB exposes PostgreSQL-compatible `pg_catalog` virtual tables (e.g., `pg_class`, `pg_namespace`, `pg_attribute`, `pg_type`) so that standard Postgres clients, ORMs, and business intelligence tools can introspect the database schema without modification. Queries against `pg_catalog.*` tables are transparently rewritten to pull from NodeDB's internal catalog.
31+
**Driver compatibility:** NodeDB advertises `server_version` (`NodeDB <version>`) and a PostgreSQL-compatible `server_version_num` in the startup parameter burst, and supports the probes drivers issue on connect: `version()` returns a PostgreSQL-compatible string, `current_setting(name [, missing_ok])` resolves the same settings as `SHOW`, and `::regclass`/`::regtype` casts, `ANY(current_schemas(...))`, and cross-catalog-table JOINs evaluate PostgreSQL-identically. Binary result formats requested at Bind are honored per column; types the encoder cannot yet emit in binary (timestamp, numeric, json/jsonb, arrays) downgrade to text, with the Describe-phase RowDescription kept in sync.
32+
33+
**Introspection:** NodeDB exposes PostgreSQL-compatible `pg_catalog` virtual tables (e.g., `pg_class`, `pg_namespace`, `pg_attribute`, `pg_type`) so that standard Postgres clients, ORMs, and business intelligence tools can introspect the database schema without modification (`psql \d`, driver type caches, ORM bootstraps). Queries against `pg_catalog.*` tables are transparently rewritten to pull from NodeDB's internal catalog.
34+
35+
**Streaming:** Unordered multi-row SELECTs stream lazily to the client — rows are not buffered and merged on the coordinator first. Ordered, aggregate, point-get, and search queries use the materialized path.
3236

3337
## NDB (Native Protocol)
3438

@@ -64,6 +68,8 @@ client.put("users", "u1", &doc).await?;
6468

6569
Use SQL for complex queries, ad-hoc exploration, and rapid prototyping. Use native methods for hot-path CRUD, vector search, and high-throughput ingest where parsing overhead matters.
6670

71+
**Transactions:** The native protocol supports full transaction blocks including savepoints (`BEGIN` / `SAVEPOINT` / `ROLLBACK TO SAVEPOINT` / `RELEASE SAVEPOINT` / `COMMIT`), sharing the same protocol-neutral session state as pgwire. Native writes issued inside a transaction stage into the same per-transaction overlay, so read-your-own-writes semantics hold across SQL and native opcodes on one connection. HTTP is stateless and does not support transaction blocks.
72+
6773
**Connection:**
6874

6975
```bash
@@ -103,6 +109,8 @@ curl http://localhost:6480/metrics
103109

104110
All non-probe routes are under `/v1/`. JSON responses carry `Content-Type: application/vnd.nodedb.v1+json; charset=utf-8`. Probes are unversioned and always reachable.
105111

112+
`/v1/query/stream` streams rows lazily as NDJSON — one line per row, produced as shards return batches. A mid-stream error surfaces in-band as a final `{"error": "..."}` line (the HTTP status stays `200` since headers are already sent). HTTP is stateless: transaction blocks (`BEGIN`/`COMMIT`) are not supported.
113+
106114
**Additional endpoints:**
107115

108116
| Endpoint | Method | Purpose |
@@ -220,12 +228,18 @@ CRDT sync protocol for NodeDB-Lite clients (mobile, WASM, desktop). Bidirectiona
220228
**Flow:**
221229

222230
1. Client connects and sends `Handshake` with JWT + vector clock
223-
2. Server responds with `HandshakeAck`
231+
2. Peer announces `CollectionSchema` for each synced collection before any shape or delta data — unknown collections are materialized into the local catalog (create-only; an existing collection is never clobbered) and propagate cluster-wide via Raft
224232
3. Server pushes `DeltaPush` messages (CRDT mutations)
225233
4. Client acknowledges with `DeltaAck`
226-
5. Conflicts rejected with `DeltaReject` + `CompensationHint`
234+
5. Constraint violations rejected with `DeltaReject` + a typed `CompensationHint`
235+
236+
**Message types:** `Handshake`, `HandshakeAck`, `CollectionSchema`, `DeltaPush`, `DeltaAck`, `DeltaReject`, `Throttle`, `PingPong`, `ResyncRequest`, `ShapeSnapshot`, `ShapeSubscribe`, `TimeseriesPush`.
237+
238+
**Durability:** An acknowledged sync write is quorum-durable — the delta is committed through the data group's Raft log before the ack, so it survives leader failover.
239+
240+
**Constraint validation:** UNIQUE, FK, required-field, and CHECK constraints are validated at apply time on Origin. A rejection carries a machine-readable `CompensationHint` (e.g., `UniqueViolation { field, conflicting_value }`, `RetryWithDifferentValue`, `ManualIntervention`) rather than a string, and a rejected delta does not wedge the stream — subsequent deltas continue to apply. Rate-limit rejections are a distinct retryable error, so clients can tell "slow down" apart from a constraint conflict.
227241

228-
**Message types:** `Handshake`, `HandshakeAck`, `DeltaPush`, `DeltaAck`, `DeltaReject`, `Throttle`, `PingPong`, `ResyncRequest`, `ShapeSnapshot`, `ShapeSubscribe`, `TimeseriesPush`.
242+
**Idempotent producers:** Every per-engine sync message carries `(producer_id, epoch, seq)` provenance; replayed deltas are acknowledged as duplicates instead of double-applied.
229243

230244
This protocol is used by NodeDB-Lite for offline-first sync. See [NodeDB-Lite](lite.md) for details.
231245

0 commit comments

Comments
 (0)