Skip to main content

contextdb_core/
table_meta.rs

1use crate::Direction;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Default, Serialize)]
6pub struct TableMeta {
7    pub columns: Vec<ColumnDef>,
8    pub immutable: bool,
9    pub state_machine: Option<StateMachineConstraint>,
10    #[serde(default)]
11    pub dag_edge_types: Vec<String>,
12    #[serde(default)]
13    pub unique_constraints: Vec<Vec<String>>,
14    pub natural_key_column: Option<String>,
15    #[serde(default)]
16    pub propagation_rules: Vec<PropagationRule>,
17    #[serde(default)]
18    pub default_ttl_seconds: Option<u64>,
19    #[serde(default)]
20    pub sync_safe: bool,
21    #[serde(default)]
22    pub expires_column: Option<String>,
23    #[serde(default)]
24    pub indexes: Vec<IndexDecl>,
25}
26
27// Custom `Deserialize` that tolerates prior on-disk `TableMeta` encoded
28// without the trailing `indexes` field (backward-compat).
29impl<'de> serde::Deserialize<'de> for TableMeta {
30    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
31    where
32        D: serde::Deserializer<'de>,
33    {
34        use serde::de::{MapAccess, SeqAccess, Visitor};
35        use std::fmt;
36
37        struct TableMetaVisitor;
38
39        impl<'de> Visitor<'de> for TableMetaVisitor {
40            type Value = TableMeta;
41
42            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43                f.write_str("a TableMeta")
44            }
45
46            fn visit_seq<A>(self, mut seq: A) -> std::result::Result<TableMeta, A::Error>
47            where
48                A: SeqAccess<'de>,
49            {
50                let columns = seq
51                    .next_element::<Vec<ColumnDef>>()?
52                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
53                let immutable = seq
54                    .next_element::<bool>()?
55                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
56                let state_machine = seq
57                    .next_element::<Option<StateMachineConstraint>>()?
58                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
59                let dag_edge_types = seq.next_element::<Vec<String>>()?.unwrap_or_default();
60                let unique_constraints =
61                    seq.next_element::<Vec<Vec<String>>>()?.unwrap_or_default();
62                let natural_key_column = seq.next_element::<Option<String>>()?.unwrap_or_default();
63                let propagation_rules = seq
64                    .next_element::<Vec<PropagationRule>>()?
65                    .unwrap_or_default();
66                let default_ttl_seconds = seq.next_element::<Option<u64>>()?.unwrap_or_default();
67                let sync_safe = seq.next_element::<bool>()?.unwrap_or_default();
68                // Ok(None) at a declared-length tail is legitimate serde
69                // behavior (declared-length sequence exhausted). Decode
70                // errors, in contrast, indicate corruption or incompatible
71                // on-disk payloads and must propagate rather than silently
72                // default.
73                let expires_column = seq.next_element::<Option<String>>()?.unwrap_or_default();
74                let indexes = seq.next_element::<Vec<IndexDecl>>()?.unwrap_or_default();
75                Ok(TableMeta {
76                    columns,
77                    immutable,
78                    state_machine,
79                    dag_edge_types,
80                    unique_constraints,
81                    natural_key_column,
82                    propagation_rules,
83                    default_ttl_seconds,
84                    sync_safe,
85                    expires_column,
86                    indexes,
87                })
88            }
89
90            fn visit_map<A>(self, mut map: A) -> std::result::Result<TableMeta, A::Error>
91            where
92                A: MapAccess<'de>,
93            {
94                let mut columns: Option<Vec<ColumnDef>> = None;
95                let mut immutable: Option<bool> = None;
96                let mut state_machine: Option<Option<StateMachineConstraint>> = None;
97                let mut dag_edge_types: Option<Vec<String>> = None;
98                let mut unique_constraints: Option<Vec<Vec<String>>> = None;
99                let mut natural_key_column: Option<Option<String>> = None;
100                let mut propagation_rules: Option<Vec<PropagationRule>> = None;
101                let mut default_ttl_seconds: Option<Option<u64>> = None;
102                let mut sync_safe: Option<bool> = None;
103                let mut expires_column: Option<Option<String>> = None;
104                let mut indexes: Option<Vec<IndexDecl>> = None;
105
106                while let Some(key) = map.next_key::<String>()? {
107                    match key.as_str() {
108                        "columns" => columns = Some(map.next_value()?),
109                        "immutable" => immutable = Some(map.next_value()?),
110                        "state_machine" => state_machine = Some(map.next_value()?),
111                        "dag_edge_types" => dag_edge_types = Some(map.next_value()?),
112                        "unique_constraints" => unique_constraints = Some(map.next_value()?),
113                        "natural_key_column" => natural_key_column = Some(map.next_value()?),
114                        "propagation_rules" => propagation_rules = Some(map.next_value()?),
115                        "default_ttl_seconds" => default_ttl_seconds = Some(map.next_value()?),
116                        "sync_safe" => sync_safe = Some(map.next_value()?),
117                        "expires_column" => expires_column = Some(map.next_value()?),
118                        "indexes" => indexes = Some(map.next_value()?),
119                        _ => {
120                            let _: serde::de::IgnoredAny = map.next_value()?;
121                        }
122                    }
123                }
124
125                Ok(TableMeta {
126                    columns: columns.ok_or_else(|| serde::de::Error::missing_field("columns"))?,
127                    immutable: immutable
128                        .ok_or_else(|| serde::de::Error::missing_field("immutable"))?,
129                    state_machine: state_machine.unwrap_or_default(),
130                    dag_edge_types: dag_edge_types.unwrap_or_default(),
131                    unique_constraints: unique_constraints.unwrap_or_default(),
132                    natural_key_column: natural_key_column.unwrap_or_default(),
133                    propagation_rules: propagation_rules.unwrap_or_default(),
134                    default_ttl_seconds: default_ttl_seconds.unwrap_or_default(),
135                    sync_safe: sync_safe.unwrap_or_default(),
136                    expires_column: expires_column.unwrap_or_default(),
137                    indexes: indexes.unwrap_or_default(),
138                })
139            }
140        }
141
142        const FIELDS: &[&str] = &[
143            "columns",
144            "immutable",
145            "state_machine",
146            "dag_edge_types",
147            "unique_constraints",
148            "natural_key_column",
149            "propagation_rules",
150            "default_ttl_seconds",
151            "sync_safe",
152            "expires_column",
153            "indexes",
154        ];
155        deserializer.deserialize_struct("TableMeta", FIELDS, TableMetaVisitor)
156    }
157}
158
159/// Direction for a column within an engine-local index declaration.
160/// Distinct from `contextdb_parser::ast::SortDirection`, which carries a
161/// `CosineDistance` variant that is meaningful only for vector ordering.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
163pub enum SortDirection {
164    #[default]
165    Asc,
166    Desc,
167}
168
169/// How an index entered `TableMeta.indexes`. `Auto` indexes are synthesized
170/// at CREATE TABLE time from PRIMARY KEY / UNIQUE constraints. `UserDeclared`
171/// indexes come from `CREATE INDEX` DDL. The distinction drives surface
172/// rendering (auto-indexes omitted from `.schema`), schema-rendering verbose
173/// flags, and sync DDL emission (auto-indexes are not re-emitted since they
174/// are derived from the CreateTable payload).
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
176pub enum IndexKind {
177    Auto,
178    #[default]
179    UserDeclared,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct IndexDecl {
184    pub name: String,
185    pub columns: Vec<(String, SortDirection)>,
186    #[serde(default)]
187    pub kind: IndexKind,
188}
189
190impl IndexDecl {
191    pub fn estimated_bytes(&self) -> usize {
192        32 + self.name.len() * 16
193            + self
194                .columns
195                .iter()
196                .fold(0usize, |acc, (c, _)| acc.saturating_add(24 + c.len() * 16))
197    }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub enum PropagationRule {
202    ForeignKey {
203        fk_column: String,
204        referenced_table: String,
205        referenced_column: String,
206        trigger_state: String,
207        target_state: String,
208        max_depth: u32,
209        abort_on_failure: bool,
210    },
211    Edge {
212        edge_type: String,
213        direction: Direction,
214        trigger_state: String,
215        target_state: String,
216        max_depth: u32,
217        abort_on_failure: bool,
218    },
219    VectorExclusion {
220        trigger_state: String,
221    },
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct StateMachineConstraint {
226    pub column: String,
227    pub transitions: HashMap<String, Vec<String>>,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
231pub struct ColumnDef {
232    pub name: String,
233    pub column_type: ColumnType,
234    pub nullable: bool,
235    pub primary_key: bool,
236    #[serde(default)]
237    pub unique: bool,
238    #[serde(default)]
239    pub default: Option<String>,
240    #[serde(default)]
241    pub references: Option<ForeignKeyReference>,
242    #[serde(default)]
243    pub expires: bool,
244    #[serde(default)]
245    pub immutable: bool,
246}
247
248// Custom `Deserialize` that tolerates prior on-disk schemas missing the
249// `immutable` field (backward-compat, I5). Attempts to deserialize the full
250// nine-field struct; on sequence truncation at the trailing `immutable`
251// position, defaults it to `false`. JSON / other formats that distinguish
252// "missing field" from "required field" continue to work via `serde(default)`
253// on the field itself.
254impl<'de> serde::Deserialize<'de> for ColumnDef {
255    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
256    where
257        D: serde::Deserializer<'de>,
258    {
259        use serde::de::{MapAccess, SeqAccess, Visitor};
260        use std::fmt;
261
262        struct ColumnDefVisitor;
263
264        impl<'de> Visitor<'de> for ColumnDefVisitor {
265            type Value = ColumnDef;
266
267            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268                f.write_str("a ColumnDef")
269            }
270
271            fn visit_seq<A>(self, mut seq: A) -> std::result::Result<ColumnDef, A::Error>
272            where
273                A: SeqAccess<'de>,
274            {
275                let name = seq
276                    .next_element::<String>()?
277                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
278                let column_type = seq
279                    .next_element::<ColumnType>()?
280                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
281                let nullable = seq
282                    .next_element::<bool>()?
283                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
284                let primary_key = seq
285                    .next_element::<bool>()?
286                    .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
287                let unique = seq.next_element::<bool>()?.unwrap_or_default();
288                let default = seq.next_element::<Option<String>>()?.unwrap_or_default();
289                let references = seq
290                    .next_element::<Option<ForeignKeyReference>>()?
291                    .unwrap_or_default();
292                let expires = seq.next_element::<bool>()?.unwrap_or_default();
293                // Trailing field. `Ok(None)` means the declared-length seq
294                // ended naturally (legitimate for JSON paths). Decode errors
295                // propagate — silently defaulting to `false` would let a
296                // corrupt payload pose as a non-immutable column.
297                let immutable = seq.next_element::<bool>()?.unwrap_or_default();
298                Ok(ColumnDef {
299                    name,
300                    column_type,
301                    nullable,
302                    primary_key,
303                    unique,
304                    default,
305                    references,
306                    expires,
307                    immutable,
308                })
309            }
310
311            fn visit_map<A>(self, mut map: A) -> std::result::Result<ColumnDef, A::Error>
312            where
313                A: MapAccess<'de>,
314            {
315                let mut name: Option<String> = None;
316                let mut column_type: Option<ColumnType> = None;
317                let mut nullable: Option<bool> = None;
318                let mut primary_key: Option<bool> = None;
319                let mut unique: Option<bool> = None;
320                let mut default: Option<Option<String>> = None;
321                let mut references: Option<Option<ForeignKeyReference>> = None;
322                let mut expires: Option<bool> = None;
323                let mut immutable: Option<bool> = None;
324
325                while let Some(key) = map.next_key::<String>()? {
326                    match key.as_str() {
327                        "name" => name = Some(map.next_value()?),
328                        "column_type" => column_type = Some(map.next_value()?),
329                        "nullable" => nullable = Some(map.next_value()?),
330                        "primary_key" => primary_key = Some(map.next_value()?),
331                        "unique" => unique = Some(map.next_value()?),
332                        "default" => default = Some(map.next_value()?),
333                        "references" => references = Some(map.next_value()?),
334                        "expires" => expires = Some(map.next_value()?),
335                        "immutable" => immutable = Some(map.next_value()?),
336                        _ => {
337                            let _: serde::de::IgnoredAny = map.next_value()?;
338                        }
339                    }
340                }
341
342                Ok(ColumnDef {
343                    name: name.ok_or_else(|| serde::de::Error::missing_field("name"))?,
344                    column_type: column_type
345                        .ok_or_else(|| serde::de::Error::missing_field("column_type"))?,
346                    nullable: nullable
347                        .ok_or_else(|| serde::de::Error::missing_field("nullable"))?,
348                    primary_key: primary_key
349                        .ok_or_else(|| serde::de::Error::missing_field("primary_key"))?,
350                    unique: unique.unwrap_or_default(),
351                    default: default.unwrap_or_default(),
352                    references: references.unwrap_or_default(),
353                    expires: expires.unwrap_or_default(),
354                    immutable: immutable.unwrap_or_default(),
355                })
356            }
357        }
358
359        const FIELDS: &[&str] = &[
360            "name",
361            "column_type",
362            "nullable",
363            "primary_key",
364            "unique",
365            "default",
366            "references",
367            "expires",
368            "immutable",
369        ];
370        deserializer.deserialize_struct("ColumnDef", FIELDS, ColumnDefVisitor)
371    }
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375pub struct ForeignKeyReference {
376    pub table: String,
377    pub column: String,
378}
379
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381pub enum ColumnType {
382    Integer,
383    Real,
384    Text,
385    Boolean,
386    Json,
387    Uuid,
388    Vector(usize),
389    Timestamp,
390    TxId,
391}
392
393impl TableMeta {
394    pub fn estimated_bytes(&self) -> usize {
395        let columns_bytes = self.columns.iter().fold(0usize, |acc, column| {
396            acc.saturating_add(column.estimated_bytes())
397        });
398        let state_machine_bytes = self
399            .state_machine
400            .as_ref()
401            .map(StateMachineConstraint::estimated_bytes)
402            .unwrap_or(0);
403        let dag_bytes = self.dag_edge_types.iter().fold(0usize, |acc, edge_type| {
404            acc.saturating_add(32 + edge_type.len() * 16)
405        });
406        let unique_constraint_bytes =
407            self.unique_constraints.iter().fold(0usize, |acc, columns| {
408                acc.saturating_add(
409                    24 + columns
410                        .iter()
411                        .map(|column| 16 + column.len() * 16)
412                        .sum::<usize>(),
413                )
414            });
415        let natural_key_bytes = self
416            .natural_key_column
417            .as_ref()
418            .map(|column| 32 + column.len() * 16)
419            .unwrap_or(0);
420        let propagation_bytes = self.propagation_rules.iter().fold(0usize, |acc, rule| {
421            acc.saturating_add(rule.estimated_bytes())
422        });
423        let expires_bytes = self
424            .expires_column
425            .as_ref()
426            .map(|column| 32 + column.len() * 16)
427            .unwrap_or(0);
428        let indexes_bytes = self
429            .indexes
430            .iter()
431            .fold(0usize, |acc, i| acc.saturating_add(i.estimated_bytes()));
432
433        16 + columns_bytes
434            + state_machine_bytes
435            + dag_bytes
436            + unique_constraint_bytes
437            + natural_key_bytes
438            + propagation_bytes
439            + expires_bytes
440            + indexes_bytes
441            + self.default_ttl_seconds.map(|_| 8).unwrap_or(0)
442            + 8
443    }
444}
445
446impl PropagationRule {
447    fn estimated_bytes(&self) -> usize {
448        match self {
449            PropagationRule::ForeignKey {
450                fk_column,
451                referenced_table,
452                referenced_column,
453                trigger_state,
454                target_state,
455                ..
456            } => {
457                24 + fk_column.len() * 16
458                    + referenced_table.len() * 16
459                    + referenced_column.len() * 16
460                    + trigger_state.len() * 16
461                    + target_state.len() * 16
462            }
463            PropagationRule::Edge {
464                edge_type,
465                trigger_state,
466                target_state,
467                ..
468            } => 24 + edge_type.len() * 16 + trigger_state.len() * 16 + target_state.len() * 16,
469            PropagationRule::VectorExclusion { trigger_state } => 16 + trigger_state.len() * 16,
470        }
471    }
472}
473
474impl StateMachineConstraint {
475    fn estimated_bytes(&self) -> usize {
476        let transitions_bytes = self.transitions.iter().fold(0usize, |acc, (from, tos)| {
477            acc.saturating_add(
478                32 + from.len() * 16 + tos.iter().map(|to| 16 + to.len() * 16).sum::<usize>(),
479            )
480        });
481        24 + self.column.len() * 16 + transitions_bytes
482    }
483}
484
485impl ColumnDef {
486    fn estimated_bytes(&self) -> usize {
487        let default_bytes = self
488            .default
489            .as_ref()
490            .map(|value| 32 + value.len() * 16)
491            .unwrap_or(0);
492        let reference_bytes = self
493            .references
494            .as_ref()
495            .map(|reference| 32 + reference.table.len() * 16 + reference.column.len() * 16)
496            .unwrap_or(0);
497        8 + self.name.len() * 16
498            + self.column_type.estimated_bytes()
499            + default_bytes
500            + reference_bytes
501            + 8
502    }
503}
504
505impl ColumnType {
506    fn estimated_bytes(&self) -> usize {
507        match self {
508            ColumnType::Integer => 16,
509            ColumnType::Real => 16,
510            ColumnType::Text => 16,
511            ColumnType::Boolean => 16,
512            ColumnType::Json => 24,
513            ColumnType::Uuid => 16,
514            ColumnType::Vector(_) => 24,
515            ColumnType::Timestamp => 16,
516            ColumnType::TxId => 8,
517        }
518    }
519}