Skip to main content

nodedb_sql/engine_rules/
timeseries.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Engine rules for timeseries collections.
4//!
5//! Timeseries is append-only. INSERT, UPDATE, and UPSERT are not supported.
6//! Data enters via INGEST (mapped to `TimeseriesIngest`).
7//! Scans route to `TimeseriesScan` for time-range-aware execution.
8
9use crate::engine_rules::*;
10use crate::error::{Result, SqlError};
11use crate::types::*;
12
13pub struct TimeseriesRules;
14
15impl EngineRules for TimeseriesRules {
16    fn plan_insert(&self, p: InsertParams) -> Result<Vec<SqlPlan>> {
17        // Timeseries INSERT routes to TimeseriesIngest — append-only semantics.
18        Ok(vec![SqlPlan::TimeseriesIngest {
19            collection: p.collection,
20            rows: p.rows,
21        }])
22    }
23
24    fn plan_upsert(&self, _p: UpsertParams) -> Result<Vec<SqlPlan>> {
25        Err(SqlError::Unsupported {
26            detail: "UPSERT is not supported on timeseries collections (append-only)".into(),
27        })
28    }
29
30    fn plan_scan(&self, p: ScanParams) -> Result<SqlPlan> {
31        if p.temporal.is_temporal() && !p.bitemporal {
32            return Err(SqlError::Unsupported {
33                detail: format!(
34                    "FOR SYSTEM_TIME / FOR VALID_TIME requires a bitemporal \
35                     timeseries collection; '{}' was not created WITH bitemporal = true",
36                    p.collection
37                ),
38            });
39        }
40        // Timeseries scans use TimeseriesScan for time-range-aware execution.
41        let time_range = default_time_range();
42        Ok(SqlPlan::TimeseriesScan {
43            collection: p.collection,
44            time_range,
45            bucket_interval_ms: 0,
46            group_by: Vec::new(),
47            aggregates: Vec::new(),
48            filters: p.filters,
49            projection: p.projection,
50            gap_fill: String::new(),
51            limit: p.limit.unwrap_or(10000),
52            tiered: false,
53            temporal: p.temporal,
54        })
55    }
56
57    fn plan_point_get(&self, _p: PointGetParams) -> Result<SqlPlan> {
58        Err(SqlError::Unsupported {
59            detail: "point lookups are not supported on timeseries collections; \
60                     use SELECT with a time range filter instead"
61                .into(),
62        })
63    }
64
65    fn plan_update(&self, _p: UpdateParams) -> Result<Vec<SqlPlan>> {
66        Err(SqlError::Unsupported {
67            detail: "UPDATE is not supported on timeseries collections; \
68                     timeseries data is append-only"
69                .into(),
70        })
71    }
72
73    fn plan_update_from(&self, p: UpdateFromParams) -> Result<Vec<SqlPlan>> {
74        Err(SqlError::Unsupported {
75            detail: format!(
76                "UPDATE ... FROM is not supported on timeseries collection '{}'; \
77                 timeseries data is append-only",
78                p.collection
79            ),
80        })
81    }
82
83    fn plan_delete(&self, p: DeleteParams) -> Result<Vec<SqlPlan>> {
84        // Timeseries supports range-based deletion (e.g. retention).
85        Ok(vec![SqlPlan::Delete {
86            collection: p.collection,
87            engine: EngineType::Timeseries,
88            filters: p.filters,
89            target_keys: p.target_keys,
90        }])
91    }
92
93    fn plan_aggregate(&self, p: AggregateParams) -> Result<SqlPlan> {
94        if p.temporal.is_temporal() && !p.bitemporal {
95            return Err(SqlError::Unsupported {
96                detail: format!(
97                    "FOR SYSTEM_TIME / FOR VALID_TIME requires a bitemporal \
98                     timeseries collection; '{}' was not created WITH bitemporal = true",
99                    p.collection
100                ),
101            });
102        }
103        Ok(SqlPlan::TimeseriesScan {
104            collection: p.collection,
105            time_range: default_time_range(),
106            bucket_interval_ms: p.bucket_interval_ms.unwrap_or(0),
107            group_by: p.group_columns,
108            aggregates: p.aggregates,
109            filters: p.filters,
110            projection: Vec::new(),
111            gap_fill: String::new(),
112            limit: p.limit,
113            tiered: p.has_auto_tier,
114            temporal: p.temporal,
115        })
116    }
117
118    fn plan_merge(&self, p: MergeParams) -> Result<Vec<SqlPlan>> {
119        Err(SqlError::Unsupported {
120            detail: format!(
121                "MERGE is not supported on timeseries collection '{}'",
122                p.collection
123            ),
124        })
125    }
126}
127
128/// Default time range bounds for the SqlPlan IR.
129///
130/// Actual time range extraction from filter predicates happens during
131/// PhysicalPlan conversion (Origin: `value::extract_time_range`).
132/// At the SqlPlan level, we pass unbounded defaults.
133fn default_time_range() -> (i64, i64) {
134    (i64::MIN, i64::MAX)
135}