Skip to content

Commit 44f8d01

Browse files
authored
Merge pull request #144 from NodeDB-Lab/feature/temporal-queries
feat: add temporal query support across engines
2 parents f052628 + 1a0b228 commit 44f8d01

134 files changed

Lines changed: 2844 additions & 981 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ jobs:
106106
# For pre-release versions, pin internal dep requirements so semver
107107
# "0.0.0" doesn't fail to match "0.0.0-beta.1"
108108
if [[ "$VERSION" == *-* ]]; then
109-
sed -i -E 's/(nodedb-(codec|bridge|wal|mem|crdt|raft|fts|types|vector|graph|spatial|strict|client|columnar|cluster|query|sql|array|physical) = \{ [^}]*version = )"[^"]*"/\1"='"$VERSION"'"/' Cargo.toml
109+
# Pin every internal nodedb* path-dep (bare `nodedb` included) to the
110+
# exact prerelease version. A generic match avoids the recurring bug
111+
# of forgetting to add a newly-introduced crate to an explicit list.
112+
sed -i -E 's/(nodedb[a-z0-9-]* = \{ [^}]*version = )"[^"]*"/\1"='"$VERSION"'"/' Cargo.toml
110113
echo "Updated internal dep versions to =$VERSION"
111114
fi
112115
@@ -233,7 +236,10 @@ jobs:
233236
# For pre-release versions, pin internal dep requirements so semver
234237
# "0" doesn't fail to match "0.0.0-beta.3"
235238
if [[ "$VERSION" == *-* ]]; then
236-
sed -i -E 's/(nodedb-(codec|bridge|wal|mem|crdt|raft|fts|types|vector|graph|spatial|strict|client|columnar|cluster|query|sql|array|physical) = \{ [^}]*version = )"[^"]*"/\1"='"$VERSION"'"/' Cargo.toml
239+
# Pin every internal nodedb* path-dep (bare `nodedb` included) to the
240+
# exact prerelease version. A generic match avoids the recurring bug
241+
# of forgetting to add a newly-introduced crate to an explicit list.
242+
sed -i -E 's/(nodedb[a-z0-9-]* = \{ [^}]*version = )"[^"]*"/\1"='"$VERSION"'"/' Cargo.toml
237243
echo "Updated internal dep versions to =$VERSION"
238244
fi
239245

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Rust build artifacts
22
/target/
3+
/releases/
34

45
# Data directories (fallback default or manual testing)
56
/data/

Cargo.lock

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/architecture.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ NodeDB splits work across three planes connected by lock-free ring buffers. This
2222
└────────────────────────┘ └─────────────────────────────────────┘
2323
```
2424

25-
**Control Plane** — Runs on Tokio. Handles connections (pgwire, HTTP, WebSocket), parses SQL via DataFusion, builds logical query plans, and dispatches work to the Data Plane. All types here are `Send + Sync`.
25+
**Control Plane** — Runs on Tokio. Handles connections (pgwire, HTTP, WebSocket), parses SQL via nodedb-sql (sqlparser-rs based), builds logical query plans, and dispatches work to the Data Plane. All types here are `Send + Sync`.
2626

2727
**Data Plane** — One thread per CPU core, each an isolated shard. Reads from NVMe via io_uring, runs SIMD vector math, executes physical query plans. No locks, no atomics, no cross-core sharing. Types are `!Send` by design. Emits `WriteEvent` records (covering inserts, updates, and deletes via `WriteOp`) to the Event Plane via per-core bounded ring buffers after each WAL commit.
2828

@@ -44,13 +44,13 @@ NodeDB splits work across three planes connected by lock-free ring buffers. This
4444

4545
There are two ways a query reaches the Data Plane. Both produce the same `PhysicalPlan` and execute identically from that point on.
4646

47-
**SQL path (user-facing)** — All user-visible interfaces use SQL. `psql`, the `ndb` CLI, and the HTTP `/v1/query` endpoint all accept SQL text. The Control Plane runs it through DataFusion (parse → logical plan → optimize → `PhysicalPlan`):
47+
**SQL path (user-facing)** — All user-visible interfaces use SQL. `psql`, the `ndb` CLI, and the HTTP `/v1/query` endpoint all accept SQL text. The Control Plane runs it through nodedb-sql (parse → logical plan → optimize → `PhysicalPlan`):
4848

4949
```
5050
psql / ndb CLI / HTTP /v1/query
5151
5252
53-
DataFusion parser
53+
nodedb-sql parser
5454
5555
5656
Logical plan + optimizer
@@ -59,7 +59,7 @@ psql / ndb CLI / HTTP /v1/query
5959
PhysicalPlan ──► SPSC Bridge ──► Data Plane
6060
```
6161

62-
**Native opcode path (SDK optimization)** — The Rust SDK (`nodedb-client`), FFI bindings (`nodedb-lite-ffi`), and WASM bindings (`nodedb-lite-wasm`) dispatch typed opcode messages over the NDB protocol instead of SQL text. The Control Plane converts them directly to a `PhysicalPlan` via `build_plan()`, skipping SQL parsing and serialization:
62+
**Native opcode path (SDK optimization)** — The Rust SDK (`nodedb-client`), FFI bindings (`nodedb-lite-ffi`), and WASM bindings (`nodedb-lite-wasm`) dispatch typed opcode messages over the NDB protocol instead of SQL text. The Control Plane converts them directly to a `PhysicalPlan` via engine-specific handlers, skipping SQL parsing and serialization:
6363

6464
```
6565
nodedb-client / nodedb-lite-ffi / nodedb-lite-wasm

docs/array.md

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,18 @@ Array schemas are defined by dimensions (axes), attributes (stored values), and
2929
```sql
3030
CREATE ARRAY spatial_grid
3131
DIMS (
32-
x INT64 DOMAIN [0, 1000),
33-
y INT64 DOMAIN [0, 1000),
34-
z INT64 DOMAIN [0, 1000)
32+
x INT64 [0..1000],
33+
y INT64 [0..1000],
34+
z INT64 [0..1000]
3535
)
3636
ATTRS (
37-
temperature FLOAT32,
38-
pressure FLOAT32,
39-
humidity FLOAT32
37+
temperature FLOAT64,
38+
pressure FLOAT64,
39+
humidity FLOAT64
4040
)
4141
TILE_EXTENTS (64, 64, 64)
4242
WITH (
43-
cell_order = 'Z-ORDER',
43+
prefix_bits = 8,
4444
audit_retain_ms = 86400000
4545
);
4646
```
@@ -49,11 +49,14 @@ CREATE ARRAY spatial_grid
4949

5050
| Parameter | Required | Default | Description |
5151
| ----------------- | -------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
52-
| `DIMS` | Yes || List of dimensions. Each has a name, type (`INT64`, `INT32`, `FLOAT64`), and domain bounds `[lo, hi)`. |
53-
| `ATTRS` | Yes || List of attributes (cell values). Each has a name and type (`FLOAT32`, FLOAT64`, `INT32`, `INT64`, `STRING`). |
52+
| `DIMS` | Yes || List of dimensions. Each has a name, type (`INT64`, `FLOAT64`, `TIMESTAMP_MS`, `STRING`), and optional domain bounds `[lo..hi]`. |
53+
| `ATTRS` | Yes || List of attributes (cell values). Each has a name and type (`INT64`, `FLOAT64`, `STRING`, `BYTES`). |
5454
| `TILE_EXTENTS` | Yes || Tuple of tile extent per dimension. All > 0. Determines cell locality and compression block granularity. |
55-
| `cell_order` | No | `'Z-ORDER'` | `'Z-ORDER'` (Hilbert curve) or `'ROW-MAJOR'`. Affects spatial cache locality. |
55+
| `CELL_ORDER` | No | `Z_ORDER` | `ROW_MAJOR`, `COL_MAJOR`, `Z_ORDER`, or `HILBERT`. Affects spatial cache locality. |
56+
| `TILE_ORDER` | No || `ROW_MAJOR`, `COL_MAJOR`, `Z_ORDER`, or `HILBERT`. Optional tile ordering. |
57+
| `prefix_bits` | No | `8` | Bits for key prefix compression (1–16). Lower = better compression, higher = faster prefix filtering. |
5658
| `audit_retain_ms` | No | `NULL` | Milliseconds. Tiles older than now - `audit_retain_ms` (by system time) are eligible for purge. `NULL` = keep all. |
59+
| `minimum_audit_retain_ms` | No || Minimum retention window enforced for compliance. |
5760

5861
## Examples
5962

@@ -63,11 +66,11 @@ CREATE ARRAY spatial_grid
6366
-- Create a 2D elevation map with tiles of 256x256 cells
6467
CREATE ARRAY elevation_map
6568
DIMS (
66-
lon FLOAT64 DOMAIN [-180, 180),
67-
lat FLOAT64 DOMAIN [-90, 90)
69+
lon FLOAT64 [-180..180],
70+
lat FLOAT64 [-90..90]
6871
)
6972
ATTRS (
70-
height FLOAT32
73+
height FLOAT64
7174
)
7275
TILE_EXTENTS (256, 256);
7376

@@ -88,13 +91,13 @@ SELECT ARRAY_FLUSH('elevation_map');
8891
-- Create a 3D climate model with temporal tracking
8992
CREATE ARRAY climate_forecast
9093
DIMS (
91-
lon INT32 DOMAIN [-180, 180),
92-
lat INT32 DOMAIN [-90, 90),
93-
altitude_m INT32 DOMAIN [0, 50000)
94+
lon INT64 [-180..180],
95+
lat INT64 [-90..90],
96+
altitude_m INT64 [0..50000]
9497
)
9598
ATTRS (
96-
temp_c FLOAT32,
97-
humidity FLOAT32
99+
temp_c FLOAT64,
100+
humidity FLOAT64
98101
)
99102
TILE_EXTENTS (32, 32, 20)
100103
WITH (audit_retain_ms = 7776000000); -- 90 days

docs/bitemporal.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ AS OF SYSTEM TIME 1700000000000
4040
AS OF VALID TIME 1700000001000;
4141
```
4242

43-
Times are milliseconds since Unix epoch. Use `extract(epoch from now()) * 1000` for current time.
43+
Times can be specified as:
44+
- Integer milliseconds since Unix epoch (e.g., `1700000000000`)
45+
- `NOW()` for current time
46+
- ISO-8601 string literal (e.g., `'2024-01-15T00:00:00Z'`)
4447

4548
## Examples
4649

@@ -50,12 +53,12 @@ Retrieve the state of a collection at a past moment:
5053

5154
```sql
5255
-- Document collection with system time tracking
53-
CREATE COLLECTION user_accounts STRICT (
56+
CREATE COLLECTION user_accounts (
5457
id UUID DEFAULT gen_uuid_v7(),
5558
email VARCHAR,
5659
balance DECIMAL,
5760
created_at TIMESTAMP DEFAULT now()
58-
);
61+
) WITH (engine='document_strict', bitemporal=true);
5962

6063
INSERT INTO user_accounts (email, balance) VALUES
6164
('alice@example.com', 100.00);
@@ -65,7 +68,7 @@ UPDATE user_accounts SET balance = 150.00 WHERE email = 'alice@example.com';
6568

6669
-- Query the database as it existed 10 minutes ago
6770
SELECT email, balance FROM user_accounts
68-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 600000);
71+
AS OF SYSTEM TIME 1700000000000;
6972
-- Returns: alice@example.com, 100.00 (the original balance)
7073

7174
-- Query current state
@@ -96,7 +99,7 @@ VALUES ('2026-04-01T10:00:00Z', 'warehouse-a', 22.3, '2026-04-02T15:30:00Z');
9699
-- Query what we knew at April 1st (before correction)
97100
SELECT location, temperature FROM sensor_readings
98101
WHERE ts BETWEEN '2026-04-01' AND '2026-04-02'
99-
AS OF VALID TIME 1711953600000; -- April 1st
102+
AS OF VALID TIME '2026-04-01T00:00:00Z';
100103

101104
-- Query what we know now (after correction)
102105
SELECT location, temperature FROM sensor_readings
@@ -127,7 +130,7 @@ SELECT lon, lat, temp_c FROM ARRAY_SLICE(
127130
{lon: [-10, 10), lat: [0, 20)},
128131
['temp_c']
129132
)
130-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 86400000);
133+
AS OF SYSTEM TIME '2026-06-06T00:00:00Z';
131134
```
132135

133136
### Lineage and Compliance
@@ -153,12 +156,14 @@ UPDATE transactions SET status = 'CONFIRMED' WHERE id = 'txn-1';
153156
UPDATE transactions SET status = 'SETTLED' WHERE id = 'txn-1';
154157

155158
-- Audit: show full timeline of this transaction
156-
SELECT status, system_time FROM transactions
159+
SELECT status, _ts_system FROM transactions
157160
WHERE id = 'txn-1'
158-
AS OF SYSTEM TIME NULL -- special: returns all versions in system time order
159-
ORDER BY system_time ASC;
161+
AS OF SYSTEM TIME NULL -- returns every system-time version, ascending
162+
ORDER BY _ts_system ASC;
160163
```
161164

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.
166+
162167
## Index Engines and Temporal Composition
163168

164169
Vector, FTS, KV, and Spatial do not carry temporal columns. This is not an oversight — it is the correct architectural decomposition. These four are **index engines**: they point at records that live in data-bearing engines. Indexes don't have time; the records they point to do.
@@ -185,7 +190,7 @@ CREATE VECTOR INDEX idx_product_vec ON product_embeddings METRIC cosine DIM 384;
185190
SELECT p.product_id, p.description,
186191
vector_distance(p.embedding, $query_vec) AS score
187192
FROM product_embeddings p
188-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 2592000000)
193+
AS OF SYSTEM TIME '2026-05-08T00:00:00Z'
189194
WHERE p.id IN (
190195
SEARCH product_embeddings USING VECTOR(embedding, $query_vec, 20)
191196
)
@@ -212,7 +217,7 @@ CREATE SEARCH INDEX ON articles FIELDS title, body ANALYZER 'english';
212217
-- 3. Bitemporal FTS: search the index state as it existed yesterday
213218
SELECT id, title
214219
FROM articles
215-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 86400000)
220+
AS OF SYSTEM TIME '2026-06-06T00:00:00Z'
216221
WHERE text_match(body, 'distributed consensus raft')
217222
ORDER BY bm25_score(body, 'distributed consensus raft') DESC
218223
LIMIT 20;
@@ -275,7 +280,7 @@ SELECT value FROM config_entries WHERE key = 'feature_x_enabled';
275280

276281
-- Audit: what was the value 7 days ago?
277282
SELECT value FROM config_entries
278-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 604800000)
283+
AS OF SYSTEM TIME '2026-05-31T00:00:00Z'
279284
WHERE key = 'feature_x_enabled';
280285
```
281286

docs/columnar.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Columnar collections track both system time and valid time, enabling corrections
166166
SELECT level, COUNT(*) FROM logs
167167
WHERE ts > now() - INTERVAL '1 hour'
168168
GROUP BY level
169-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 3600000);
169+
AS OF SYSTEM TIME '2026-06-07T12:00:00Z';
170170

171171
-- Query records valid at a specific date (forecast corrections, backdated data)
172172
SELECT host, cpu_usage FROM metrics

docs/documents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ Both schemaless and strict documents support bitemporal queries — tracking sys
167167
```sql
168168
-- Query documents as they existed yesterday (system time)
169169
SELECT * FROM users
170-
AS OF SYSTEM TIME (extract(epoch from now()) * 1000 - 86400000);
170+
AS OF SYSTEM TIME '2026-06-06T00:00:00Z';
171171

172172
-- Query documents that were valid at a past date (valid time)
173173
SELECT * FROM users

docs/full-text-search.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,20 @@ CREATE SEARCH INDEX ON articles FIELDS title, body
3939
ANALYZER 'english'
4040
FUZZY true;
4141

42-
-- Basic search
42+
-- Basic search with text_match
4343
SELECT title, bm25_score(body, 'distributed database rust') AS score
4444
FROM articles
4545
WHERE text_match(body, 'distributed database rust')
4646
ORDER BY score DESC
4747
LIMIT 20;
4848

49+
-- Using SEARCH() as an alias (equivalent to text_match)
50+
SELECT title, bm25_score(body, 'distributed database rust') AS score
51+
FROM articles
52+
WHERE SEARCH(body, 'distributed database rust')
53+
ORDER BY score DESC
54+
LIMIT 20;
55+
4956
-- Fuzzy search (handles typos)
5057
SELECT title FROM articles
5158
WHERE text_match(title, 'databse', { fuzzy: true, distance: 2 });

0 commit comments

Comments
 (0)