Skip to content

Commit 8975153

Browse files
committed
feat(nodedb): wire bitemporal temporal scope through columnar and timeseries physical plans
Add `system_as_of_ms` and `valid_at_ms` fields to `ColumnarOp::Scan` and `TimeseriesOp::Scan` in the physical plan. All non-bitemporal construction sites (auto-tier, aggregate convert, native plan builders, wire helpers, alert executor) initialise both fields to `None`. In `sql_plan_convert`, propagate `temporal.system_as_of_ms` and the valid-time point derived from `temporal` into the physical scan for plain-columnar and timeseries paths; spatial and KV paths continue to pass `None`. Thread `temporal` through `TimeseriesScanParams` so the timeseries conversion function has access to the scope.
1 parent ba4e6b9 commit 8975153

11 files changed

Lines changed: 53 additions & 0 deletions

File tree

nodedb/src/bridge/physical_plan/columnar.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ pub enum ColumnarOp {
6161
/// against matching rows before the `limit` is enforced. Empty
6262
/// for scans with no ORDER BY.
6363
sort_keys: Vec<(String, bool)>,
64+
/// Bitemporal system-time cutoff: read rows whose `_ts_system`
65+
/// is ≤ this value. `None` = current-state read. Only populated
66+
/// for collections created `WITH BITEMPORAL`.
67+
#[serde(default)]
68+
#[msgpack(default)]
69+
system_as_of_ms: Option<i64>,
70+
/// Bitemporal valid-time predicate: keep rows whose
71+
/// `[_ts_valid_from, _ts_valid_until)` interval contains this
72+
/// point. `None` = no valid-time filter.
73+
#[serde(default)]
74+
#[msgpack(default)]
75+
valid_at_ms: Option<i64>,
6476
},
6577

6678
/// Insert rows into a columnar memtable.

nodedb/src/bridge/physical_plan/timeseries.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ pub enum TimeseriesOp {
3939
computed_columns: Vec<u8>,
4040
/// RLS post-scan filters (applied after time-range pruning).
4141
rls_filters: Vec<u8>,
42+
/// Bitemporal system-time cutoff. When `Some`, block-skip on
43+
/// `_ts_system` min/max and a post-fetch filter exclude rows
44+
/// written after the given epoch-ms. Only populated for
45+
/// timeseries collections created `WITH BITEMPORAL`.
46+
#[serde(default)]
47+
#[msgpack(default)]
48+
system_as_of_ms: Option<i64>,
49+
/// Bitemporal valid-time point. When `Some`, only rows whose
50+
/// `[_ts_valid_from, _ts_valid_until)` interval contains this
51+
/// point are returned.
52+
#[serde(default)]
53+
#[msgpack(default)]
54+
valid_at_ms: Option<i64>,
4255
},
4356

4457
/// Write a batch of samples to the columnar memtable.

nodedb/src/bridge/physical_plan/wire.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ mod tests {
150150
filters: vec![],
151151
rls_filters: vec![],
152152
sort_keys: vec![],
153+
system_as_of_ms: None,
154+
valid_at_ms: None,
153155
}));
154156
}
155157

@@ -167,6 +169,8 @@ mod tests {
167169
gap_fill: "null".into(),
168170
computed_columns: vec![],
169171
rls_filters: vec![],
172+
system_as_of_ms: None,
173+
valid_at_ms: None,
170174
}));
171175
}
172176

nodedb/src/control/planner/auto_tier.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ fn build_scan_task(
225225
gap_fill: gap_fill.to_string(),
226226
computed_columns: Vec::new(),
227227
rls_filters: Vec::new(),
228+
system_as_of_ms: None,
229+
valid_at_ms: None,
228230
}),
229231
post_set_op: PostSetOp::None,
230232
}

nodedb/src/control/planner/sql_plan_convert/aggregate.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ pub(super) fn convert_aggregate(
112112
gap_fill: String::new(),
113113
computed_columns: Vec::new(),
114114
rls_filters: Vec::new(),
115+
system_as_of_ms: None,
116+
valid_at_ms: None,
115117
}),
116118
post_set_op: PostSetOp::None,
117119
}]);

nodedb/src/control/planner/sql_plan_convert/convert.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ pub(super) fn convert_one(
210210
gap_fill,
211211
limit,
212212
tiered,
213+
temporal,
213214
} => super::scan::convert_timeseries_scan(super::scan_params::TimeseriesScanParams {
214215
collection,
215216
time_range,
@@ -223,6 +224,7 @@ pub(super) fn convert_one(
223224
tiered,
224225
tenant_id,
225226
ctx,
227+
temporal,
226228
}),
227229

228230
SqlPlan::TimeseriesIngest { collection, rows } => {

nodedb/src/control/planner/sql_plan_convert/scan.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ pub(super) fn convert_scan(p: ScanParams<'_>) -> crate::Result<Vec<PhysicalTask>
103103
gap_fill: String::new(),
104104
computed_columns: computed_bytes,
105105
rls_filters: Vec::new(),
106+
system_as_of_ms: None,
107+
valid_at_ms: None,
106108
})
107109
}
108110
EngineType::Columnar => PhysicalPlan::Columnar(ColumnarOp::Scan {
@@ -112,6 +114,8 @@ pub(super) fn convert_scan(p: ScanParams<'_>) -> crate::Result<Vec<PhysicalTask>
112114
filters: filter_bytes,
113115
rls_filters: Vec::new(),
114116
sort_keys: sort.clone(),
117+
system_as_of_ms: temporal.system_as_of_ms,
118+
valid_at_ms: valid_at_from_scope(temporal),
115119
}),
116120
EngineType::Spatial => PhysicalPlan::Columnar(ColumnarOp::Scan {
117121
collection: collection.into(),
@@ -120,6 +124,8 @@ pub(super) fn convert_scan(p: ScanParams<'_>) -> crate::Result<Vec<PhysicalTask>
120124
filters: filter_bytes,
121125
rls_filters: Vec::new(),
122126
sort_keys: sort.clone(),
127+
system_as_of_ms: None,
128+
valid_at_ms: None,
123129
}),
124130
EngineType::KeyValue => PhysicalPlan::Kv(KvOp::Scan {
125131
collection: collection.into(),
@@ -237,6 +243,8 @@ pub(super) fn convert_point_get(
237243
filters: filter_bytes,
238244
rls_filters: Vec::new(),
239245
sort_keys: Vec::new(),
246+
system_as_of_ms: None,
247+
valid_at_ms: None,
240248
})
241249
}
242250
// Timeseries should never reach here — nodedb-sql rejects point gets.
@@ -341,6 +349,7 @@ pub(super) fn convert_timeseries_scan(
341349
tiered,
342350
tenant_id,
343351
ctx,
352+
temporal,
344353
} = p;
345354
let filter_bytes = serialize_filters(filters)?;
346355
let agg_pairs: Vec<(String, String)> = aggregates.iter().map(agg_expr_to_pair).collect();
@@ -379,6 +388,8 @@ pub(super) fn convert_timeseries_scan(
379388
gap_fill: gap_fill.to_string(),
380389
computed_columns: Vec::new(),
381390
rls_filters: Vec::new(),
391+
system_as_of_ms: temporal.system_as_of_ms,
392+
valid_at_ms: valid_at_from_scope(temporal),
382393
}),
383394
post_set_op: PostSetOp::None,
384395
}])

nodedb/src/control/planner/sql_plan_convert/scan_params.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub(super) struct TimeseriesScanParams<'a> {
6161
pub tiered: &'a bool,
6262
pub tenant_id: TenantId,
6363
pub ctx: &'a ConvertContext,
64+
pub temporal: &'a nodedb_sql::TemporalScope,
6465
}
6566

6667
/// Parameters for `convert_hybrid_search`.

nodedb/src/control/server/native/dispatch/plan_builder/columnar.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub(crate) fn build_scan(fields: &TextFields, collection: &str) -> crate::Result
1616
filters,
1717
rls_filters: Vec::new(),
1818
sort_keys: Vec::new(),
19+
system_as_of_ms: None,
20+
valid_at_ms: None,
1921
}))
2022
}
2123

nodedb/src/control/server/native/dispatch/plan_builder/timeseries.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub(crate) fn build_scan(fields: &TextFields, collection: &str) -> crate::Result
3131
gap_fill: String::new(),
3232
computed_columns: Vec::new(),
3333
rls_filters: Vec::new(),
34+
system_as_of_ms: None,
35+
valid_at_ms: None,
3436
}))
3537
}
3638

0 commit comments

Comments
 (0)