llkv_table/
ddl.rs

1//! Shared catalog DDL trait for contexts that can execute plan-based schema operations.
2//!
3//! This trait lives in `llkv-table` so that the runtime, transaction, and session layers
4//! can agree on a single contract without introducing circular dependencies. It captures
5//! the common plan-driven schema mutations (create/drop table, rename table, create/drop
6//! index) while letting each layer choose its own return types via associated outputs.
7
8use llkv_plan::{
9    AlterTablePlan, CreateIndexPlan, CreateTablePlan, CreateViewPlan, DropIndexPlan, DropTablePlan,
10    DropViewPlan, RenameTablePlan,
11};
12use llkv_result::Result;
13use sqlparser::ast::{CreateTable, ObjectName, ObjectNamePart, TableConstraint};
14use std::collections::HashSet;
15
16/// Common DDL operations that operate on catalog-backed tables and indexes.
17///
18/// Implementors provide plan-based entry points for schema mutations so higher layers
19/// (runtime session, transaction staging, etc.) can share the same method signatures.
20/// Associated output types keep the contract generic while still conveying the concrete
21/// results used by each layer (e.g., `RuntimeStatementResult`, `TransactionResult`, or
22/// unit for operations that only signal success).
23pub trait CatalogDdl {
24    /// Output of executing a CREATE TABLE plan.
25    type CreateTableOutput;
26    /// Output of executing a DROP TABLE plan.
27    type DropTableOutput;
28    /// Output of executing a table rename.
29    type RenameTableOutput;
30    /// Output of executing an ALTER TABLE plan.
31    type AlterTableOutput;
32    /// Output of executing a CREATE INDEX plan.
33    type CreateIndexOutput;
34    /// Output of executing a DROP INDEX plan.
35    type DropIndexOutput;
36
37    /// Creates a table described by the given plan.
38    fn create_table(&self, plan: CreateTablePlan) -> Result<Self::CreateTableOutput>;
39
40    /// Drops a table identified by the given plan.
41    fn drop_table(&self, plan: DropTablePlan) -> Result<Self::DropTableOutput>;
42
43    /// Creates a view described by the given plan.
44    fn create_view(&self, plan: CreateViewPlan) -> Result<()>;
45
46    /// Drops a view identified by the given plan.
47    fn drop_view(&self, plan: DropViewPlan) -> Result<()>;
48
49    /// Renames a table using the provided plan.
50    fn rename_table(&self, plan: RenameTablePlan) -> Result<Self::RenameTableOutput>;
51
52    /// Alters a table using the provided plan.
53    fn alter_table(&self, plan: AlterTablePlan) -> Result<Self::AlterTableOutput>;
54
55    /// Creates an index described by the given plan.
56    fn create_index(&self, plan: CreateIndexPlan) -> Result<Self::CreateIndexOutput>;
57
58    /// Drops a single-column index described by the given plan, returning metadata about the
59    /// index when it existed.
60    fn drop_index(&self, plan: DropIndexPlan) -> Result<Self::DropIndexOutput>;
61}
62
63/// Extension methods for working with `ObjectName`.
64pub trait ObjectNameExt {
65    /// Return the final identifier component in uppercase form.
66    fn canonical_ident(&self) -> Option<String>;
67}
68
69impl ObjectNameExt for ObjectName {
70    fn canonical_ident(&self) -> Option<String> {
71        self.0.last().and_then(|part| match part {
72            ObjectNamePart::Identifier(ident) => Some(ident.value.to_ascii_uppercase()),
73            ObjectNamePart::Function(_) => None,
74        })
75    }
76}
77
78/// Extension methods for normalizing `TableConstraint` instances.
79pub trait TableConstraintExt {
80    fn normalize(self) -> TableConstraint;
81}
82
83impl TableConstraintExt for TableConstraint {
84    fn normalize(self) -> TableConstraint {
85        match self {
86            TableConstraint::ForeignKey {
87                mut name,
88                mut index_name,
89                columns,
90                foreign_table,
91                referred_columns,
92                on_delete,
93                on_update,
94                characteristics,
95            } => {
96                if name.is_none() {
97                    name = index_name.clone();
98                }
99                index_name = None;
100                TableConstraint::ForeignKey {
101                    name,
102                    index_name,
103                    columns,
104                    foreign_table,
105                    referred_columns,
106                    on_delete,
107                    on_update,
108                    characteristics,
109                }
110            }
111            TableConstraint::PrimaryKey {
112                name,
113                index_name: _,
114                index_type,
115                columns,
116                index_options,
117                characteristics,
118            } => TableConstraint::PrimaryKey {
119                name,
120                index_name: None,
121                index_type,
122                columns,
123                index_options,
124                characteristics,
125            },
126            TableConstraint::Unique {
127                name,
128                index_name: _,
129                index_type,
130                columns,
131                index_options,
132                nulls_distinct,
133                characteristics,
134                index_type_display,
135            } => TableConstraint::Unique {
136                name,
137                index_name: None,
138                index_type,
139                columns,
140                index_options,
141                nulls_distinct,
142                characteristics,
143                index_type_display,
144            },
145            other => other,
146        }
147    }
148}
149
150/// Order CREATE TABLE statements such that referenced tables appear before dependents.
151pub trait OrderCreateTablesExt {
152    fn order_by_foreign_keys(self) -> Vec<CreateTable>;
153}
154
155impl OrderCreateTablesExt for Vec<CreateTable> {
156    fn order_by_foreign_keys(mut self) -> Vec<CreateTable> {
157        let mut ordered = Vec::with_capacity(self.len());
158        if self.is_empty() {
159            return ordered;
160        }
161
162        let table_names: HashSet<String> = self
163            .iter()
164            .filter_map(|table| table.name.canonical_ident())
165            .collect();
166
167        let mut resolved: HashSet<String> = HashSet::new();
168
169        while !self.is_empty() {
170            let mut progress = false;
171            let current_round = std::mem::take(&mut self);
172            let mut next_round = Vec::new();
173
174            for table in current_round.into_iter() {
175                let table_name = table.name.canonical_ident().unwrap_or_default();
176                let deps = collect_foreign_key_dependencies(&table);
177                let deps_satisfied = deps.into_iter().all(|dep| {
178                    dep.is_empty()
179                        || dep == table_name
180                        || !table_names.contains(&dep)
181                        || resolved.contains(&dep)
182                });
183
184                if deps_satisfied {
185                    resolved.insert(table_name);
186                    ordered.push(table);
187                    progress = true;
188                } else {
189                    next_round.push(table);
190                }
191            }
192
193            if !progress {
194                ordered.append(&mut next_round);
195                break;
196            }
197
198            self = next_round;
199        }
200
201        ordered
202    }
203}
204
205fn collect_foreign_key_dependencies(table: &CreateTable) -> Vec<String> {
206    let mut deps = Vec::new();
207    for constraint in &table.constraints {
208        if let TableConstraint::ForeignKey { foreign_table, .. } = constraint
209            && let Some(name) = foreign_table.canonical_ident()
210        {
211            deps.push(name);
212        }
213    }
214    deps
215}