systemprompt_database/lifecycle/installation/fk_deferral/
mod.rs1mod emit;
23
24use pg_query::NodeEnum;
25use pg_query::protobuf::node::Node;
26use pg_query::protobuf::{ColumnDef, ConstrType, Constraint, CreateStmt};
27use thiserror::Error;
28
29use emit::{deferred_key, string_node};
30
31#[derive(Debug, Error)]
32pub enum FkDeferralError {
33 #[error("SQL parse failed: {0}")]
34 Parse(#[source] pg_query::Error),
35 #[error("not a CREATE TABLE statement")]
36 NotCreateTable,
37 #[error("could not re-emit CREATE TABLE {table} without its foreign keys: {source}")]
38 DeparseTable {
39 table: String,
40 #[source]
41 source: pg_query::Error,
42 },
43 #[error("could not emit the deferred foreign key on {table}: {source}")]
44 DeparseKey {
45 table: String,
46 #[source]
47 source: pg_query::Error,
48 },
49 #[error("foreign key on {table} names no referenced table")]
50 NoReferencedTable { table: String },
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct DeferredForeignKey {
55 pub table: String,
56 pub source_table: String,
57 pub columns: Vec<String>,
58 pub referenced_table: String,
59 pub referenced_columns: Vec<String>,
60 pub constraint_name: String,
61 pub sql: String,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct SplitCreateTable {
66 pub create_table_sql: String,
67 pub foreign_keys: Vec<DeferredForeignKey>,
68}
69
70pub(super) fn split_foreign_keys(
71 original_sql: &str,
72 create: &CreateStmt,
73) -> Result<SplitCreateTable, FkDeferralError> {
74 let Some(relation) = create.relation.as_ref() else {
75 return Ok(verbatim(original_sql));
76 };
77
78 let mut stripped = create.clone();
79 let mut constraints: Vec<Constraint> = Vec::new();
80 stripped.table_elts = create
81 .table_elts
82 .iter()
83 .filter_map(|elt| match elt.node.as_ref() {
84 Some(Node::Constraint(c)) if is_foreign(c) => {
85 constraints.push((**c).clone());
86 None
87 },
88 Some(Node::ColumnDef(cd)) => {
89 let (column, mut found) = strip_column_references(cd);
90 constraints.append(&mut found);
91 Some(pg_query::protobuf::Node {
92 node: Some(Node::ColumnDef(Box::new(column))),
93 })
94 },
95 _ => Some(elt.clone()),
96 })
97 .collect();
98
99 if constraints.is_empty() {
100 return Ok(verbatim(original_sql));
101 }
102
103 let create_table_sql = NodeEnum::CreateStmt(stripped).deparse().map_err(|source| {
104 FkDeferralError::DeparseTable {
105 table: relation.relname.clone(),
106 source,
107 }
108 })?;
109
110 let foreign_keys = constraints
111 .into_iter()
112 .map(|c| deferred_key(relation, c))
113 .collect::<Result<Vec<_>, _>>()?;
114
115 Ok(SplitCreateTable {
116 create_table_sql,
117 foreign_keys,
118 })
119}
120
121pub fn split_create_table_foreign_keys(sql: &str) -> Result<SplitCreateTable, FkDeferralError> {
122 let parsed = pg_query::parse(sql).map_err(FkDeferralError::Parse)?;
123 let create = parsed
124 .protobuf
125 .stmts
126 .iter()
127 .find_map(|raw| match raw.stmt.as_ref()?.node.as_ref()? {
128 Node::CreateStmt(create) => Some(create),
129 _ => None,
130 })
131 .ok_or(FkDeferralError::NotCreateTable)?;
132 split_foreign_keys(sql, create)
133}
134
135fn verbatim(original_sql: &str) -> SplitCreateTable {
136 SplitCreateTable {
137 create_table_sql: original_sql.to_owned(),
138 foreign_keys: Vec::new(),
139 }
140}
141
142fn is_foreign(c: &Constraint) -> bool {
143 ConstrType::try_from(c.contype) == Ok(ConstrType::ConstrForeign)
144}
145
146fn strip_column_references(cd: &ColumnDef) -> (ColumnDef, Vec<Constraint>) {
147 let mut column = (*cd).clone();
148 let mut kept = Vec::with_capacity(cd.constraints.len());
149 let mut found = Vec::new();
150 let mut last_primary_is_foreign = false;
151
152 for node in &cd.constraints {
153 let Some(Node::Constraint(c)) = node.node.as_ref() else {
154 kept.push(node.clone());
155 continue;
156 };
157 if is_foreign(c) {
158 let mut key = (**c).clone();
159 key.fk_attrs = vec![string_node(&cd.colname)];
160 found.push(key);
161 last_primary_is_foreign = true;
162 continue;
163 }
164 if last_primary_is_foreign
170 && let Some(previous) = found.last_mut()
171 && fold_attribute(previous, c)
172 {
173 continue;
174 }
175 if is_key_like(c) {
176 last_primary_is_foreign = false;
177 }
178 kept.push(node.clone());
179 }
180
181 column.constraints = kept;
182 (column, found)
183}
184
185fn is_key_like(c: &Constraint) -> bool {
186 matches!(
187 ConstrType::try_from(c.contype),
188 Ok(ConstrType::ConstrPrimary | ConstrType::ConstrUnique | ConstrType::ConstrExclusion)
189 )
190}
191
192fn fold_attribute(key: &mut Constraint, attribute: &Constraint) -> bool {
193 match ConstrType::try_from(attribute.contype) {
194 Ok(ConstrType::ConstrAttrDeferrable) => key.deferrable = true,
195 Ok(ConstrType::ConstrAttrNotDeferrable) => key.deferrable = false,
196 Ok(ConstrType::ConstrAttrDeferred) => {
197 key.deferrable = true;
198 key.initdeferred = true;
199 },
200 Ok(ConstrType::ConstrAttrImmediate) => key.initdeferred = false,
201 _ => return false,
202 }
203 true
204}