From 2249f48ad6b194962d02f9278c4dbef2a9486f0a Mon Sep 17 00:00:00 2001 From: Hatta Zainal Date: Wed, 8 Jul 2026 09:24:18 +0800 Subject: [PATCH] docs: update for transactions, distributed execution, and protocol changes 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 --- docs/architecture.md | 23 +++++++++-- docs/bitemporal.md | 4 +- docs/databases.md | 2 + docs/full-text-search.md | 2 +- docs/graph.md | 15 ++++++- docs/offline-sync-patterns.md | 6 +++ docs/protocols.md | 22 ++++++++-- docs/query-language.md | 75 ++++++++++++++++++++++++++++++++--- docs/security/auth.md | 24 +++++++++++ docs/spatial.md | 7 ++++ 10 files changed, 164 insertions(+), 16 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index a30f0c168..5496bc58c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. @@ -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 ` 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. @@ -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 diff --git a/docs/bitemporal.md b/docs/bitemporal.md index b3877f8b2..7ea151a12 100644 --- a/docs/bitemporal.md +++ b/docs/bitemporal.md @@ -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 diff --git a/docs/databases.md b/docs/databases.md index 12ea27bcf..dbeb2db10 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -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. diff --git a/docs/full-text-search.md b/docs/full-text-search.md index de347356f..2dc570f3d 100644 --- a/docs/full-text-search.md +++ b/docs/full-text-search.md @@ -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 ''` or `ALTER COLLECTION ... SET text_analyzer = ''`). 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. diff --git a/docs/graph.md b/docs/graph.md index c70b2f292..08e8e99b9 100644 --- a/docs/graph.md +++ b/docs/graph.md @@ -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 @@ -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 diff --git a/docs/offline-sync-patterns.md b/docs/offline-sync-patterns.md index 2128aa4ae..c66838111 100644 --- a/docs/offline-sync-patterns.md +++ b/docs/offline-sync-patterns.md @@ -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) diff --git a/docs/protocols.md b/docs/protocols.md index 8abe0547e..da68cf471 100644 --- a/docs/protocols.md +++ b/docs/protocols.md @@ -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 `) 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) @@ -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 @@ -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 | @@ -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. diff --git a/docs/query-language.md b/docs/query-language.md index 26f16af7b..77eb39f5a 100644 --- a/docs/query-language.md +++ b/docs/query-language.md @@ -22,11 +22,13 @@ The Rust SDK (`nodedb-client`), FFI bindings (`nodedb-lite-ffi`), and WASM bindi SELECT [DISTINCT] FROM [WHERE ] -[GROUP BY ] [HAVING ] +[GROUP BY ] [HAVING ] [ORDER BY [ASC|DESC], ...] [LIMIT ] [OFFSET ] ``` +An unbounded `SELECT` (no `LIMIT`) is capped by a per-core memory budget (`[tuning.query] max_scan_result_bytes`, default 512 MiB) rather than a silent row cap. Exceeding the budget returns a typed `ResourcesExhausted` error suggesting a `LIMIT` — results are never silently truncated. Unordered multi-row SELECTs stream lazily to the client on pgwire, native, and HTTP. + ### Filtering ```sql @@ -57,8 +59,19 @@ GROUP BY status HAVING COUNT(*) > 5; SELECT COUNT(DISTINCT user_id) FROM orders; + +-- Computed expressions as GROUP BY keys +SELECT UPPER(label) AS u, SUM(score) FROM events GROUP BY UPPER(label); + +-- GROUP BY a SELECT alias or ordinal also works +SELECT UPPER(label) AS u, SUM(score) FROM events GROUP BY u; +SELECT UPPER(label), SUM(score) FROM events GROUP BY 1; ``` +GROUP BY keys follow PostgreSQL semantics: output columns appear in SELECT-list order, and a key projected as `SELECT k AS label` is emitted under its alias. + +A scalar aggregate (no GROUP BY) over zero input rows returns one identity row: `COUNT(*)` → `0`; `SUM`/`AVG`/`MIN`/`MAX` → `NULL`. A `GROUP BY` aggregate over zero rows returns zero rows. + ### Joins ```sql @@ -332,6 +345,9 @@ CREATE COLLECTION orders ( CREATE COLLECTION sessions (key TEXT PRIMARY KEY) WITH (engine='kv'); -- extra columns are optional typed value fields +-- Trailing ENGINE suffix (MySQL-style) is equivalent to WITH (engine='...') +CREATE COLLECTION metrics (ts TIMESTAMP TIME_KEY, cpu FLOAT) ENGINE = timeseries; + -- Graph edges are overlays on document collections, not a separate collection type. -- Use GRAPH INSERT EDGE to add edges between documents in any collection. @@ -375,7 +391,7 @@ listing every dependent. #### Columnar Family DDL -`columnar`, `timeseries`, and `spatial` are peer engines sharing the same compressed-column storage core. Pick one per collection via `WITH (engine='')`. Column modifiers designate special columns: +`columnar`, `timeseries`, and `spatial` are peer engines sharing the same compressed-column storage core. Pick one per collection via `WITH (engine='')` or the trailing `ENGINE = ` suffix (the two forms are equivalent; specifying both with different values is rejected). Valid engine names: `document_schemaless`, `document_strict`, `kv`, `columnar`, `timeseries`, `spatial`, `vector`. Column modifiers designate special columns: | Modifier | Column type | Effect | | --------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------- | @@ -608,10 +624,21 @@ COPY tenant_restore(acme) FROM STDIN DRY RUN; -- Restore COPY tenant_restore(acme) FROM STDIN; +-- SQL form (server-side path) +RESTORE TENANT acme FROM '/backups/acme.ndb'; +RESTORE TENANT acme FROM '/backups/acme.ndb' DRY RUN; + +-- Bypass the staleness guard +RESTORE TENANT acme FROM '/backups/acme.ndb' FORCE; +-- COPY form accepts the same modifiers: +COPY tenant_restore(acme) FROM STDIN FORCE; + -- Remove ALL tenant data across all engines and caches (requires CONFIRM) PURGE TENANT acme CONFIRM; ``` +**Staleness guard.** A restore is refused when the backup's watermark is older than the cluster's last observed write for that tenant — newer writes would be silently overwritten. `FORCE` overrides the guard (and logs that newer writes will be overwritten). `FORCE` and `DRY RUN` are independent modifiers and may appear in either order. + ### Indexes ```sql @@ -809,6 +836,12 @@ SELECT * FROM locations WHERE ST_Contains(region, geom); SELECT * FROM locations WHERE ST_Intersects(geom, boundary); SELECT * FROM locations WHERE ST_Within(geom, area); SELECT ST_Distance(a.geom, b.geom) FROM locations a, locations b; + +-- Geometry constructors in INSERT VALUES +INSERT INTO locations (id, geom) VALUES ('p1', ST_MakePoint(-122.4, 37.8)); +INSERT INTO locations (id, geom) VALUES ('p2', ST_GeomFromText('POINT(-122.4 37.8)')); +INSERT INTO locations (id, geom) VALUES ('l1', ST_GeomFromText('LINESTRING(-122.4 37.8, -121.0 36.5)')); +INSERT INTO locations (id, geom) VALUES ('w1', ST_GeomFromWKB(X'0101000000...')); ``` ### Document Navigation @@ -927,11 +960,25 @@ ROLLBACK; BEGIN; SAVEPOINT sp1; INSERT INTO users (id, name) VALUES ('u1', 'Alice'); -ROLLBACK TO sp1; +ROLLBACK TO SAVEPOINT sp1; -- the SAVEPOINT keyword is optional +RELEASE SAVEPOINT sp1; -- also: RELEASE sp1 COMMIT; ``` -Isolation level: **Snapshot Isolation (SI)**. Reads see a consistent snapshot from `BEGIN` time. Write conflicts detected at `COMMIT`. +Isolation level: **Snapshot Isolation (SI)**. Reads see a consistent snapshot from `BEGIN` time. Write conflicts detected at `COMMIT` and surfaced as SQLSTATE `40001` (`could not serialize access due to concurrent update`). + +### Read-Your-Own-Writes + +Statements inside `BEGIN ... COMMIT` are staged in a per-transaction overlay on the Data Plane. Reads within the same transaction observe staged writes **across every engine**: KV, document (schemaless and strict), columnar, timeseries, graph (single-hop, multi-hop, shortest path), vector search, spatial predicates, full-text search, and hybrid search fusion. Constraint violations (e.g., primary-key uniqueness) surface immediately at statement time, not at COMMIT. + +### Savepoints + +Savepoints work on pgwire and the native protocol (HTTP is stateless — no transaction blocks). `ROLLBACK TO SAVEPOINT` rewinds the staging overlay itself via an undo journal — staged inserts disappear from reads, updates restore the prior staged value, deletes make rows reappear, and graph-edge staging reverts. Rollback undo covers index side-effects too: secondary indexes, FTS postings, HNSW vectors, R-tree entries, and column statistics. + +| Error | SQLSTATE | +| -------------------------------------------- | -------- | +| `ROLLBACK TO` / `RELEASE` unknown savepoint | `3B001` | +| Savepoint outside a transaction | `25P01` | ### Atomic Transfers @@ -989,12 +1036,24 @@ COPY users FROM STDIN WITH (FORMAT csv); -- Query plan EXPLAIN SELECT * FROM users WHERE age > 30; +-- Collect per-column statistics for the cost-based planner +-- (row_count, distinct_count, avg_value_len; used to pick distributed +-- join/aggregate strategies). Auto-ANALYZE re-runs after ~10% of rows change. +ANALYZE orders; +ANALYZE; -- all collections + -- Session variables SET nodedb.consistency = 'eventual'; SHOW nodedb.consistency; SHOW ALL; RESET nodedb.consistency; +-- Server version (PostgreSQL-compatible) +SELECT version(); -- 'PostgreSQL 15 ... NodeDB ...' +SHOW server_version_num; +SELECT current_setting('server_version_num'); +SELECT current_setting('nodedb.foo', true); -- missing_ok: NULL if unset + -- Roles and permissions SHOW ROLES; @@ -1034,7 +1093,7 @@ CREATE ROLE IF NOT EXISTS analyst; CREATE USER IF NOT EXISTS alice PASSWORD 'secret'; DROP ROLE analyst; DROP ROLE IF EXISTS analyst; -DROP USER alice; +DROP USER alice; -- owned objects reassigned to {tenant}_admin, grants revoked DROP USER IF EXISTS alice; GRANT READ ON analytics TO analyst; GRANT ROLE analyst TO alice; @@ -1090,6 +1149,10 @@ SHOW USERS; `CAST(expr AS type)`, `TRY_CAST(expr AS type)`, `expr::type` +### System + +`version()`, `current_setting(name [, missing_ok])` + ### KV Atomic `KV_INCR(collection, key, delta [, TTL => secs])`, `KV_DECR(collection, key, delta)`, `KV_INCR_FLOAT(collection, key, delta)`, `KV_CAS(collection, key, expected, new_value)`, `KV_GETSET(collection, key, new_value)` @@ -1116,7 +1179,7 @@ SHOW USERS; | ----------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `WITH RECURSIVE` | Supported | Iterative fixed-point execution. For graph traversal, the native `GRAPH TRAVERSE`, `GRAPH PATH`, and algorithm functions remain more efficient. | | `UPDATE/DELETE ... JOIN` | Not supported | The Data Plane executes mutations as single-collection atomic operations through the SPSC bridge. Multi-collection mutations would require cross-engine coordination that breaks the isolation model. Rewrite as a subquery: `DELETE FROM orders WHERE user_id IN (SELECT id FROM users WHERE ...)`. | -| `FOREIGN KEY` | Not enforced | In a distributed system with CRDT sync and eventual consistency at the edge, enforcing FK constraints across collections would require cross-shard coordination on every write — killing write throughput. CRDT constraint validation (UNIQUE, FK) is enforced at Raft commit time for synced collections, but not for general SQL. | +| `FOREIGN KEY` | Not enforced | In a distributed system with CRDT sync and eventual consistency at the edge, enforcing FK constraints across collections would require cross-shard coordination on every write — killing write throughput. CRDT constraint validation (UNIQUE, FK, CHECK) is enforced at delta-apply time for synced collections, but not for general SQL. | | `COPY TO` (export) | Not supported | The Data Plane is write-optimized with io_uring for ingest, but export requires serialization across all shards and cores. Use the HTTP API (`/v1/query/stream`) for NDJSON export or query into Parquet via L2 cold storage. | | `UPDATE/DELETE` on timeseries | Not supported | Timeseries collections use append-only columnar memtables with cascading compression (ALP + FastLanes + FSST + Gorilla + LZ4). In-place mutation would break compression chains and invalidate block statistics. Use retention policies to age out old data. | | `EXPLAIN ANALYZE` | Not yet | Requires instrumentation across the SPSC bridge to collect per-core execution stats from the Data Plane and merge them on the Control Plane. The bridge currently returns results but not timing metadata. Planned. | diff --git a/docs/security/auth.md b/docs/security/auth.md index 6423c5175..c96695733 100644 --- a/docs/security/auth.md +++ b/docs/security/auth.md @@ -140,6 +140,30 @@ client_ca = "/path/to/ca.crt" # enables mTLS crl = "/path/to/revocation.crl" # optional CRL ``` +## Dropping Users + +`DROP USER` is safe against dangling references. Before the user row is deleted: + +- Every object the user owns — collections, functions, procedures, triggers, materialized views, sequences, schedules, change streams, continuous aggregates, indexes — is reassigned to the tenant admin (`{tenant}_admin`). +- All grants held by the user are revoked. + +The operation is fail-closed: if any reassignment fails, the user is not deleted, so a partially-cleaned user can never leave orphaned owner references. + +```sql +DROP USER alice; +DROP USER IF EXISTS alice; +``` + +## Login Rate Limiting + +Pre-auth rate limits protect against brute-force and Argon2/SCRAM CPU exhaustion: + +- **Per-IP failure cap** — default 30 failed attempts/min (`cluster.login_attempts_per_ip_per_min`) +- **Per-user failure cap** — default 10 failed attempts/min (`cluster.login_attempts_per_user_per_min`) +- **Per-IP verification ceiling** — `max(ip_cap × 4, 120)` credential verifications/min, consumed on every attempt (successful or not) to bound password-hashing CPU + +Only genuine credential failures consume the failure budgets — a burst of successful reconnects (e.g., a warming connection pool) is never rejected. Set a cap to `0` to disable it. A rate-limit rejection returns a distinct, retryable `TOO_MANY_CONNECTIONS` error rather than a generic credential failure, so clients can distinguish "slow down and retry" from "bad password." All denials take constant time with a uniform delay to prevent timing leaks. + ## Auth Priority When multiple methods are configured, NodeDB checks in order: diff --git a/docs/spatial.md b/docs/spatial.md index 0b21287ec..d1b554622 100644 --- a/docs/spatial.md +++ b/docs/spatial.md @@ -18,6 +18,7 @@ Spatial is a **columnar profile**. Collections with a `SPATIAL_INDEX` column mod - **Geohash** — Encode/decode, neighbor cells, area covering - **H3 hexagonal index** — Uber's H3 via h3o for uniform-area spatial binning - **OGC predicates** — `ST_Contains`, `ST_Intersects`, `ST_Within`, `ST_DWithin`, `ST_Distance`, `ST_Intersection`, `ST_Buffer`, `ST_Envelope`, `ST_Union` +- **Geometry constructors** — `ST_MakePoint`, `ST_GeomFromText` (WKT), `ST_GeomFromWKB`, `ST_GeomFromGeoJSON` — usable directly in `INSERT VALUES` - **Format support** — WKB, WKT, GeoJSON interchange. GeoParquet v1.1.0 + GeoArrow metadata. - **Hybrid spatial-vector** — Spatial R\*-tree narrows candidates by location, then HNSW ranks by semantic similarity in one query - **Spatial join** — R\*-tree probe join between two collections @@ -46,6 +47,12 @@ INSERT INTO restaurants { rating: 4.5 }; +-- Insert with geometry constructors (WKT, WKB, or coordinates) +INSERT INTO restaurants (name, location) VALUES + ('Taco Stand', ST_MakePoint(-73.982, 40.742)), + ('Pho House', ST_GeomFromText('POINT(-73.978 40.751)')), + ('Deli', ST_GeomFromWKB(X'01010000000000000000000040000000000000F03F')); + -- Find restaurants within 1km SELECT name, rating, ST_Distance(location, ST_Point(-73.990, 40.750)) AS dist_m FROM restaurants