1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
//! HTAP bridge integration tests.
//!
//! Tests the CDC pipeline from strict document collections to columnar
//! materialized views, query routing, and consistency controls. The
//! columnar insert/scan path is exercised by the 0.1.0 gates; HTAP
//! materialized-view routing tested here is not part of those gates
//! (see `docs/lite-support-matrix.md`).
use std::sync::Arc;
use nodedb_client::NodeDb;
use nodedb_lite::{NodeDbLite, PagedbStorageMem};
use nodedb_types::value::Value;
async fn open_db() -> Arc<NodeDbLite<PagedbStorageMem>> {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
NodeDbLite::open(storage).await.unwrap()
}
// ═══════════════════════════════════════════════════════════════════════
// CDC correctness
// ═══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn cdc_replicates_inserts_to_materialized_view() {
let db = open_db().await;
// Create strict source collection.
db.execute_sql(
"CREATE COLLECTION customers (
id BIGINT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
balance FLOAT64
) WITH storage = 'strict'",
&[],
)
.await
.unwrap();
// Create materialized view.
db.execute_sql(
"CREATE MATERIALIZED VIEW customer_analytics FROM customers",
&[],
)
.await
.unwrap();
// Insert into strict — should replicate to columnar via CDC.
db.strict_insert(
"customers",
&[
Value::Integer(1),
Value::String("Alice".into()),
Value::Float(1000.0),
],
)
.await
.unwrap();
db.strict_insert(
"customers",
&[
Value::Integer(2),
Value::String("Bob".into()),
Value::Float(2000.0),
],
)
.await
.unwrap();
// Verify: columnar materialized view received the rows.
let columnar = db.columnar_engine();
assert_eq!(columnar.row_count("customer_analytics"), 2);
}
// ═══════════════════════════════════════════════════════════════════════
// CDC lag
// ═══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn cdc_lag_is_bounded() {
let db = open_db().await;
db.execute_sql(
"CREATE COLLECTION orders (
id BIGINT NOT NULL PRIMARY KEY,
total FLOAT64
) WITH storage = 'strict'",
&[],
)
.await
.unwrap();
db.execute_sql("CREATE MATERIALIZED VIEW order_analytics FROM orders", &[])
.await
.unwrap();
// Insert a batch.
for i in 0..100 {
db.strict_insert(
"orders",
&[Value::Integer(i), Value::Float(i as f64 * 10.0)],
)
.await
.unwrap();
}
// In Lite, CDC is synchronous — lag should be minimal.
// The HTAP bridge tracks last_replicated_ms.
let htap = db.htap_bridge();
let lag = htap.lag_ms("order_analytics");
// Synchronous CDC: lag should be < 1 second.
assert!(lag < 1000, "CDC lag too high: {lag}ms");
}
// ═══════════════════════════════════════════════════════════════════════
// Default routing safety
// ═══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn default_routing_goes_to_source() {
let db = open_db().await;
db.execute_sql(
"CREATE COLLECTION accounts (
id BIGINT NOT NULL PRIMARY KEY,
name TEXT NOT NULL
) WITH storage = 'strict'",
&[],
)
.await
.unwrap();
db.execute_sql(
"CREATE MATERIALIZED VIEW account_analytics FROM accounts",
&[],
)
.await
.unwrap();
db.strict_insert(
"accounts",
&[Value::Integer(1), Value::String("Test".into())],
)
.await
.unwrap();
// With default read_source='source', a SELECT should go to the strict
// collection, NOT the materialized view. The strict collection is
// registered as the table provider by default.
let result = db
.execute_sql("SELECT * FROM accounts WHERE id = 1", &[])
.await;
// Should succeed — reading from the source strict collection.
assert!(result.is_ok(), "query should succeed: {result:?}");
}
// ═══════════════════════════════════════════════════════════════════════
// Auto routing correctness
// ═══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn auto_routing_for_analytical_queries() {
let db = open_db().await;
db.execute_sql(
"CREATE COLLECTION sales (
id BIGINT NOT NULL PRIMARY KEY,
amount FLOAT64,
region TEXT
) WITH storage = 'strict'",
&[],
)
.await
.unwrap();
db.execute_sql("CREATE MATERIALIZED VIEW sales_analytics FROM sales", &[])
.await
.unwrap();
// Insert data.
for i in 0..10 {
db.strict_insert(
"sales",
&[
Value::Integer(i),
Value::Float(i as f64 * 100.0),
Value::String(if i % 2 == 0 { "east" } else { "west" }.into()),
],
)
.await
.unwrap();
}
// Flush the columnar materialized view so it has segments to scan.
db.columnar_engine()
.flush_collection("sales_analytics")
.await
.unwrap();
// An analytical query (GROUP BY) should work.
// The HTAP routing detects GROUP BY and routes to the columnar view.
// Note: the columnar table provider needs flushed segments to scan.
let columnar = db.columnar_engine();
let count = columnar.row_count("sales_analytics");
assert_eq!(count, 10, "materialized view should have 10 rows");
}
// ═══════════════════════════════════════════════════════════════════════
// Strong materialized consistency
// ═══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn strong_consistency_flushes_before_read() {
let db = open_db().await;
db.execute_sql(
"CREATE COLLECTION inventory (
id BIGINT NOT NULL PRIMARY KEY,
quantity BIGINT
) WITH storage = 'strict'",
&[],
)
.await
.unwrap();
db.execute_sql(
"CREATE MATERIALIZED VIEW inventory_report FROM inventory",
&[],
)
.await
.unwrap();
// Insert data — CDC replicates synchronously in Lite.
for i in 0..5 {
db.strict_insert("inventory", &[Value::Integer(i), Value::Integer(i * 100)])
.await
.unwrap();
}
// The materialized view should have the data immediately (synchronous CDC).
let columnar = db.columnar_engine();
assert_eq!(columnar.row_count("inventory_report"), 5);
// In Lite, "strong consistency" is the default because CDC is synchronous.
// There's no lag between source writes and materialized view updates.
}
// ═══════════════════════════════════════════════════════════════════════
// Staleness annotation
// ═══════════════════════════════════════════════════════════════════════
#[tokio::test]
async fn staleness_tracking() {
let db = open_db().await;
db.execute_sql(
"CREATE COLLECTION telemetry (
id BIGINT NOT NULL PRIMARY KEY,
value FLOAT64
) WITH storage = 'strict'",
&[],
)
.await
.unwrap();
db.execute_sql(
"CREATE MATERIALIZED VIEW telemetry_report FROM telemetry",
&[],
)
.await
.unwrap();
// Insert data.
db.strict_insert("telemetry", &[Value::Integer(1), Value::Float(42.0)])
.await
.unwrap();
// Check staleness annotation via HTAP bridge.
let htap = db.htap_bridge();
let view = htap.view_by_target("telemetry_report");
assert!(view.is_some());
let view = view.unwrap();
assert_eq!(view.rows_replicated, 1);
assert!(view.last_replicated_ms > 0);
// Lag should be near-zero for synchronous CDC.
let lag = htap.lag_ms("telemetry_report");
assert!(lag < 1000, "lag should be minimal: {lag}ms");
}