Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ NodeDB uses Multi-Raft — each vShard is its own independent Raft group. Three

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

**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.

**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.

## Cross-Shard Transactions

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.
Expand All @@ -150,6 +154,18 @@ SET cross_shard_txn = 'best_effort_non_atomic';

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

## Distributed Query Execution

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).

**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).

**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`.

**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).

**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.

## Cross-Engine Queries

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.
Expand Down Expand Up @@ -268,10 +284,11 @@ user:{id} → org:{id} → tenant:{id} → database:{id}

The database bucket capacity is `database.quota.max_qps`. Additionally, pre-auth login rate limits are enforced:

- `login_ip:{addr}` — capacity `cluster.login_attempts_per_ip_per_min` (default 30)
- `login_user:{username}` — capacity `cluster.login_attempts_per_user_per_min` (default 10)
- `login_ip:{addr}` — capacity `cluster.login_attempts_per_ip_per_min` (default 30 **failures**/min)
- `login_user:{username}` — capacity `cluster.login_attempts_per_user_per_min` (default 10 **failures**/min)
- Argon2-DoS ceiling per IP — `max(ip_cap × 4, 120)` credential verifications/min, consumed on every attempt

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.
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.

### Per-Tenant Compute Caps

Expand Down
4 changes: 3 additions & 1 deletion docs/bitemporal.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ AS OF SYSTEM TIME NULL -- returns every system-time version, ascending
ORDER BY _ts_system ASC;
```

`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.
`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.

**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.

## Index Engines and Temporal Composition

Expand Down
2 changes: 2 additions & 0 deletions docs/databases.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ Accessing a collection in a different database than your bound database returns

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

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.

## Quotas

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.
Expand Down
2 changes: 1 addition & 1 deletion docs/full-text-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ NodeDB's full-text search engine provides Block-Max WAND (BMW) optimized BM25 ra
- **Korean Hangul decomposition** — Decomposes Hangul syllables into Jamo components for morphological matching.
- **AND-first with OR fallback** — Tries AND semantics first; if zero results, falls back to OR with a coverage penalty (`matched_terms / total_terms`).
- **Phrase proximity boost** — Consecutive query tokens at consecutive positions get up to 3x score boost.
- **Per-collection analyzer binding** — Each collection can configure its analyzer and language. Applied consistently at both index time and query time, eliminating mismatch bugs.
- **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.
- **Field-aware BM25** — Weighted multi-field scoring: `final_score = Σ(weight_i × bm25(field_i))`. Title matches score higher than body matches.
- **Fuzzy matching** — Levenshtein distance-based matching with adaptive thresholds (1 edit for 4-6 chars, 2 for 7+).
- **Synonyms** — Define synonym groups for query-time expansion.
Expand Down
15 changes: 14 additions & 1 deletion docs/graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ RETURN DISTINCT related.id, related.description;

## Distributed Execution (BSP)

In multi-shard deployments, graph algorithms and pattern matching execute via Bulk Synchronous Parallel (BSP) coordination.
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).

### Distributed PageRank

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

### Variable-Length Expansion Caps

Variable-length `[*min..max]` MATCH expansion is bounded by operational knobs in the `[tuning.graph]` section of `config.toml`:

| Knob | Default | Effect |
| -------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `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. |
| `varlen_max_frontier`| 100000 | Cap on the live per-hop frontier; overflow pages via resume. |
| `max_depth` | 10 | Maximum traversal depth. |
| `max_visited` | 100000 | Memory budget on visited nodes. |

Resume cursors work both locally and across shards, so a capped expansion continues from where it stopped instead of failing.

---

## Bitemporal Support
Expand Down
6 changes: 6 additions & 0 deletions docs/offline-sync-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ LIMIT 1;

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.

**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.

**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.

**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.

## NodeDB-Lite Usage

### Creating Offline Invoices (Swift/Kotlin via FFI)
Expand Down
22 changes: 18 additions & 4 deletions docs/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ psql -h localhost -p 6432

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

**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.
**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.

**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.

**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.

## NDB (Native Protocol)

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

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.

**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.

**Connection:**

```bash
Expand Down Expand Up @@ -103,6 +109,8 @@ curl http://localhost:6480/metrics

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.

`/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.

**Additional endpoints:**

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

1. Client connects and sends `Handshake` with JWT + vector clock
2. Server responds with `HandshakeAck`
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
3. Server pushes `DeltaPush` messages (CRDT mutations)
4. Client acknowledges with `DeltaAck`
5. Conflicts rejected with `DeltaReject` + `CompensationHint`
5. Constraint violations rejected with `DeltaReject` + a typed `CompensationHint`

**Message types:** `Handshake`, `HandshakeAck`, `CollectionSchema`, `DeltaPush`, `DeltaAck`, `DeltaReject`, `Throttle`, `PingPong`, `ResyncRequest`, `ShapeSnapshot`, `ShapeSubscribe`, `TimeseriesPush`.

**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.

**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.

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

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

Expand Down
Loading
Loading