Skip to main content

nodedb_sql/engine_rules/
kv.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Engine rules for key-value collections.
4
5use crate::engine_rules::*;
6use crate::error::{Result, SqlError};
7use crate::types::*;
8
9pub struct KvRules;
10
11impl EngineRules for KvRules {
12    fn plan_insert(&self, p: InsertParams) -> Result<Vec<SqlPlan>> {
13        // KV INSERT is handled separately via KvInsert plan variant.
14        // The caller (dml.rs) builds KvInsert directly because it needs
15        // to split key vs value columns. This method handles the case
16        // where a generic INSERT reaches KV — which should not happen
17        // because dml.rs checks engine type before calling engine_rules.
18        // But if it does, we error clearly.
19        Err(SqlError::Unsupported {
20            detail: format!(
21                "INSERT into KV collection '{}' must use the KV insert path",
22                p.collection
23            ),
24        })
25    }
26
27    fn plan_insert_select(&self, p: InsertSelectParams) -> Result<Vec<SqlPlan>> {
28        Err(SqlError::Unsupported {
29            detail: format!(
30                "INSERT ... SELECT into key-value collection '{}' is not supported; \
31                 the target must be a document collection",
32                p.collection
33            ),
34        })
35    }
36
37    fn plan_upsert(&self, _p: UpsertParams) -> Result<Vec<SqlPlan>> {
38        // KV upsert is handled via the KvInsert path (KV PUT is naturally an upsert).
39        // Same as plan_insert — KV INSERT uses KvInsert which is already upsert semantics.
40        Err(SqlError::Unsupported {
41            detail: "KV UPSERT must use the KV insert path (KV PUT is naturally an upsert)".into(),
42        })
43    }
44
45    fn plan_scan(&self, p: ScanParams) -> Result<SqlPlan> {
46        if p.temporal.is_temporal() {
47            return Err(SqlError::Unsupported {
48                detail: format!(
49                    "FOR SYSTEM_TIME / FOR VALID_TIME is not supported on KV collection '{}'",
50                    p.collection
51                ),
52            });
53        }
54        Ok(SqlPlan::Scan {
55            collection: p.collection,
56            alias: p.alias,
57            engine: EngineType::KeyValue,
58            filters: p.filters,
59            projection: p.projection,
60            sort_keys: p.sort_keys,
61            limit: p.limit,
62            offset: p.offset,
63            distinct: p.distinct,
64            window_functions: p.window_functions,
65            temporal: p.temporal,
66        })
67    }
68
69    fn plan_point_get(&self, p: PointGetParams) -> Result<SqlPlan> {
70        Ok(SqlPlan::PointGet {
71            collection: p.collection,
72            alias: p.alias,
73            engine: EngineType::KeyValue,
74            key_column: p.key_column,
75            key_value: p.key_value,
76            projection: p.projection,
77        })
78    }
79
80    fn plan_update(&self, p: UpdateParams) -> Result<Vec<SqlPlan>> {
81        Ok(vec![SqlPlan::Update {
82            collection: p.collection,
83            engine: EngineType::KeyValue,
84            assignments: p.assignments,
85            filters: p.filters,
86            target_keys: p.target_keys,
87            returning: p.returning,
88        }])
89    }
90
91    fn plan_update_from(&self, p: UpdateFromParams) -> Result<Vec<SqlPlan>> {
92        Err(SqlError::Unsupported {
93            detail: format!(
94                "UPDATE ... FROM is not supported on KV collection '{}'; \
95                 KV keys are opaque and cannot participate in cross-table join updates",
96                p.collection
97            ),
98        })
99    }
100
101    fn plan_delete(&self, p: DeleteParams) -> Result<Vec<SqlPlan>> {
102        Ok(vec![SqlPlan::Delete {
103            collection: p.collection,
104            engine: EngineType::KeyValue,
105            filters: p.filters,
106            target_keys: p.target_keys,
107        }])
108    }
109
110    fn plan_truncate(&self, p: TruncateParams) -> Result<Vec<SqlPlan>> {
111        Ok(vec![SqlPlan::Truncate {
112            collection: p.collection,
113            engine: EngineType::KeyValue,
114            restart_identity: p.restart_identity,
115        }])
116    }
117
118    fn plan_aggregate(&self, p: AggregateParams) -> Result<SqlPlan> {
119        let base_scan = SqlPlan::Scan {
120            collection: p.collection,
121            alias: p.alias,
122            engine: EngineType::KeyValue,
123            filters: p.filters,
124            projection: Vec::new(),
125            sort_keys: Vec::new(),
126            limit: None,
127            offset: 0,
128            distinct: false,
129            window_functions: Vec::new(),
130            temporal: crate::temporal::TemporalScope::default(),
131        };
132        Ok(SqlPlan::Aggregate {
133            input: Box::new(base_scan),
134            group_by: p.group_by,
135            group_by_aliases: Vec::new(),
136            output_order: Vec::new(),
137            aggregates: p.aggregates,
138            having: p.having,
139            limit: p.limit,
140            grouping_sets: p.grouping_sets,
141            sort_keys: Vec::new(),
142        })
143    }
144
145    fn plan_merge(&self, p: MergeParams) -> Result<Vec<SqlPlan>> {
146        Err(SqlError::Unsupported {
147            detail: format!(
148                "MERGE is not supported on key-value collection '{}'",
149                p.collection
150            ),
151        })
152    }
153
154    /// The key/value/ttl shape is fixed.
155    fn accepts_undeclared_columns(&self) -> bool {
156        false
157    }
158
159    /// The key/value/ttl triple is the engine's fixed shape, so a collection
160    /// carries all three even when the DDL names only the key.
161    fn implicit_columns(&self) -> &'static [&'static str] {
162        &["key", "value", "ttl"]
163    }
164
165    fn value_column_name_is_free(&self) -> bool {
166        true
167    }
168}