Skip to main content

linkrs_query/cache/plan_cache/
key.rs

1use crate::planning::plan::execution_plan::PartitionSpec;
2use std::hash::Hasher;
3
4/// Parameter location information
5#[derive(Debug, Clone)]
6pub struct ParamPosition {
7    /// Parameter Index
8    pub index: usize,
9    /// Parameter name (named parameter)
10    pub name: Option<String>,
11    /// Position of the parameter in the query
12    pub position: usize,
13    /// Desired data types
14    pub expected_type: Option<linkrs_core::types::DataType>,
15}
16
17/// Context stored with a cached plan.
18#[derive(Debug, Clone, Default)]
19pub struct PlanCachePutContext {
20    pub dependent_tables: Vec<String>,
21    pub space_name: Option<String>,
22    pub schema_version: Option<u64>,
23    pub index_version: Option<u64>,
24    pub is_dml: bool,
25    pub is_transaction: bool,
26    pub optimizer_version: u64,
27    pub planning_config_hash: u64,
28}
29
30/// Complete request/catalog/runtime context used for every plan-cache operation.
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub struct PlanCacheContext {
33    pub space_name: Option<String>,
34    pub schema_version: Option<u64>,
35    pub index_version: Option<u64>,
36    pub param_type_signature: Option<u64>,
37    pub optimizer_version: u64,
38    pub planning_config_hash: u64,
39}
40
41/// Query Plan Cache Key
42///
43/// Supports fast lookups using the hash of the query text as the key.
44/// Also store query text for conflict detection.
45///
46/// When a plan carries a non-stale `PartitionSpec` the key includes a
47/// partition fingerprint so that a change in the underlying data layout
48/// (e.g. re-indexing) automatically yields a cache miss, forcing the
49/// planner to produce a fresh physical plan.  Callers that only have
50/// the query text (the common lookup path before planning) query with
51/// `fingerprint == None`, which will *not* match entries that were stored
52/// with a fingerprint — effectively isolating partitioned plans from the
53/// cache until the caller can provide the current layout version.
54///
55/// Cache key now includes optional `space_name` and `schema_version`
56/// to prevent cross-space plan reuse and stale plans after schema changes.
57///
58/// Cache key includes optional `param_type_signature` so that the
59/// same query text with different parameter type signatures produces a
60/// different key, but different parameter *values* do not (allowing cached
61/// plan reuse across executions with different values of the same types).
62#[derive(Debug, Clone, PartialEq, Eq, Hash)]
63pub struct PlanCacheKey {
64    /// Query the hash value of the text
65    pub hash: u64,
66    /// Query text (for conflict detection, not just debugging)
67    pub(crate) query_text: String,
68    /// Partition layout fingerprint.  `Some` when the cached plan holds a
69    /// `PartitionSpec`; absent for single-tree plans.
70    pub(crate) partition_fingerprint: Option<u64>,
71    /// Space/catalog identity — prevents cross-space plan reuse.
72    pub(crate) space_name: Option<String>,
73    /// Schema version at planning time — forces replan after DDL.
74    pub(crate) schema_version: Option<u64>,
75    /// Parameter type signature — prevents reuse when param types differ.
76    /// Does NOT include parameter values, only their declared types.
77    pub(crate) param_type_signature: Option<u64>,
78    /// Index version at planning time — forces replan after index DDL.
79    pub(crate) index_version: Option<u64>,
80    pub(crate) optimizer_version: u64,
81    pub(crate) planning_config_hash: u64,
82}
83
84impl PlanCacheKey {
85    /// Creating Cache Keys from Query Text
86    pub fn from_query(query: &str) -> Self {
87        let hash = Self::hash_query(query);
88        Self {
89            hash,
90            query_text: query.to_string(),
91            partition_fingerprint: None,
92            space_name: None,
93            schema_version: None,
94            param_type_signature: None,
95            index_version: None,
96            optimizer_version: 0,
97            planning_config_hash: 0,
98        }
99    }
100
101    /// Create a key scoped to a specific space and schema version.
102    ///
103    /// `param_type_signature` is a hash of the parameter types (not
104    /// values) so the same query with different param values but same types
105    /// reuses the cached plan.
106    pub fn from_query_with_space(
107        query: &str,
108        space_name: Option<String>,
109        schema_version: Option<u64>,
110    ) -> Self {
111        Self::from_query_with_full_context(query, space_name, schema_version, None, None)
112    }
113
114    /// Create a key with all available context dimensions.
115    pub fn from_query_with_full_context(
116        query: &str,
117        space_name: Option<String>,
118        schema_version: Option<u64>,
119        param_type_signature: Option<u64>,
120        index_version: Option<u64>,
121    ) -> Self {
122        Self::from_query_with_context(
123            query,
124            PlanCacheContext {
125                space_name,
126                schema_version,
127                index_version,
128                param_type_signature,
129                ..Default::default()
130            },
131        )
132    }
133
134    pub fn from_query_with_context(query: &str, context: PlanCacheContext) -> Self {
135        use std::hash::Hash;
136
137        let mut hasher = Self::hasher();
138        Self::normalize_query(query).hash(&mut hasher);
139        if let Some(ref name) = context.space_name {
140            name.hash(&mut hasher);
141        }
142        context.schema_version.hash(&mut hasher);
143        context.param_type_signature.hash(&mut hasher);
144        context.index_version.hash(&mut hasher);
145        context.optimizer_version.hash(&mut hasher);
146        context.planning_config_hash.hash(&mut hasher);
147        let hash = hasher.finish();
148
149        Self {
150            hash,
151            query_text: query.to_string(),
152            partition_fingerprint: None,
153            space_name: context.space_name,
154            schema_version: context.schema_version,
155            param_type_signature: context.param_type_signature,
156            index_version: context.index_version,
157            optimizer_version: context.optimizer_version,
158            planning_config_hash: context.planning_config_hash,
159        }
160    }
161
162    /// Create a key that additionally captures a physical partition layout,
163    /// preventing stale cached plans from being reused after layout changes.
164    pub fn from_query_with_partition(query: &str, spec: &PartitionSpec) -> Self {
165        Self::from_query_with_partition_and_context(query, spec, PlanCacheContext::default())
166    }
167
168    /// Create a partition-scoped key that also carries the full compatibility
169    /// context (space / schema / index / param types / optimizer / planning
170    /// config).  Used so partitioned plans are invalidated on DDL, cross-space
171    /// reuse is blocked, and a changed layout fingerprint forces a replan.
172    pub fn from_query_with_partition_and_context(
173        query: &str,
174        spec: &PartitionSpec,
175        context: PlanCacheContext,
176    ) -> Self {
177        use std::hash::Hash;
178
179        let fp = Self::compute_fingerprint(spec);
180
181        let mut hasher = Self::hasher();
182        Self::normalize_query(query).hash(&mut hasher);
183        fp.hash(&mut hasher);
184        if let Some(ref name) = context.space_name {
185            name.hash(&mut hasher);
186        }
187        context.schema_version.hash(&mut hasher);
188        context.param_type_signature.hash(&mut hasher);
189        context.index_version.hash(&mut hasher);
190        context.optimizer_version.hash(&mut hasher);
191        context.planning_config_hash.hash(&mut hasher);
192        let hash = hasher.finish();
193
194        Self {
195            hash,
196            query_text: query.to_string(),
197            partition_fingerprint: Some(fp),
198            space_name: context.space_name,
199            schema_version: context.schema_version,
200            param_type_signature: context.param_type_signature,
201            index_version: context.index_version,
202            optimizer_version: context.optimizer_version,
203            planning_config_hash: context.planning_config_hash,
204        }
205    }
206
207    fn hash_query(query: &str) -> u64 {
208        use std::hash::Hash;
209        let mut hasher = Self::hasher();
210        Self::normalize_query(query).hash(&mut hasher);
211        hasher.finish()
212    }
213
214    /// Canonicalize insignificant whitespace for cache identity while retaining
215    /// the original query text for collision diagnostics.
216    /// Intentionally distinct from pattern normalization (`?` grouping used
217    /// by feedback/slow-query): cache keys must distinguish literal values
218    /// because different constants can yield different optimal plans.
219    fn normalize_query(query: &str) -> String {
220        query.split_whitespace().collect::<Vec<_>>().join(" ")
221    }
222
223    fn hasher() -> std::collections::hash_map::DefaultHasher {
224        std::collections::hash_map::DefaultHasher::new()
225    }
226
227    /// Produce a numeric fingerprint from the partition ranges, source, and
228    /// layout version so that any change in the range layout, the data domain
229    /// it maps over, or the storage layout version yields a different key.
230    fn compute_fingerprint(spec: &PartitionSpec) -> u64 {
231        use std::hash::Hash;
232        let mut hasher = Self::hasher();
233        for range in spec.ranges() {
234            range.start.hash(&mut hasher);
235            range.end.hash(&mut hasher);
236        }
237        spec.source().to_string().hash(&mut hasher);
238        spec.layout_version().hash(&mut hasher);
239        hasher.finish()
240    }
241
242    /// Verify that the query text matches (for conflict detection)
243    pub fn verify_query(&self, query: &str) -> bool {
244        self.query_text == query
245    }
246
247    /// Get query text (for debugging or logging)
248    pub fn query_text(&self) -> &str {
249        &self.query_text
250    }
251
252    /// Whether this key holds a partition fingerprint.
253    pub fn has_partition_fingerprint(&self) -> bool {
254        self.partition_fingerprint.is_some()
255    }
256
257    /// Return the space name, if any.
258    pub fn space_name(&self) -> Option<&str> {
259        self.space_name.as_deref()
260    }
261
262    /// Return the schema version, if any.
263    pub fn schema_version(&self) -> Option<u64> {
264        self.schema_version
265    }
266}