systemprompt_database/lifecycle/installation/
backfill.rs1use std::collections::BTreeSet;
35
36use pg_query::NodeEnum;
37use pg_query::protobuf::{self, Node};
38
39pub const PROJECTION_BACKLOG: &str = "projection_backlog";
40
41#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct BackfillStatement {
46 pub position: usize,
47 pub target: String,
48 pub form: &'static str,
49}
50
51impl BackfillStatement {
52 #[must_use]
53 pub fn is_projection_cleanup(&self, projection_tables: &BTreeSet<String>) -> bool {
54 self.form == "DELETE"
55 && (self.target == PROJECTION_BACKLOG || projection_tables.contains(&self.target))
56 }
57}
58
59#[must_use]
60pub fn projection_tables<S: AsRef<str>>(schemas: impl IntoIterator<Item = S>) -> BTreeSet<String> {
61 let mut queued = BTreeSet::new();
62 let mut created = BTreeSet::new();
63 for sql in schemas {
64 let sql = sql.as_ref();
65 queued.extend(queued_projections(sql));
66 created.extend(created_tables(sql));
67 }
68 queued.intersection(&created).cloned().collect()
69}
70
71fn queued_projections(sql: &str) -> Vec<String> {
74 const CALL: &str = "queue_projection_backlog(";
75 sql.match_indices(CALL)
76 .filter_map(|(at, _)| {
77 let rest = sql[at + CALL.len()..].trim_start().strip_prefix('\'')?;
78 let end = rest.find('\'')?;
79 Some(rest[..end].to_owned())
80 })
81 .collect()
82}
83
84fn created_tables(sql: &str) -> Vec<String> {
85 let Ok(parsed) = pg_query::parse(sql) else {
86 return Vec::new();
87 };
88 parsed
89 .protobuf
90 .stmts
91 .iter()
92 .filter_map(|raw| match raw.stmt.as_ref()?.node.as_ref()? {
93 NodeEnum::CreateStmt(stmt) => Some(stmt.relation.as_ref()?.relname.clone()),
94 _ => None,
95 })
96 .collect()
97}
98
99#[must_use]
100pub fn backfill_statements(sql: &str) -> Vec<BackfillStatement> {
101 let Ok(parsed) = pg_query::parse(sql) else {
102 return Vec::new();
103 };
104 parsed
105 .protobuf
106 .stmts
107 .iter()
108 .filter_map(|raw| raw.stmt.as_ref().and_then(|s| s.node.as_ref()))
109 .enumerate()
110 .filter_map(|(index, node)| {
111 classify(node).map(|(target, form)| BackfillStatement {
112 position: index + 1,
113 target,
114 form,
115 })
116 })
117 .collect()
118}
119
120fn classify(node: &NodeEnum) -> Option<(String, &'static str)> {
121 match node {
122 NodeEnum::UpdateStmt(stmt) => Some((stmt.relation.as_ref()?.relname.clone(), "UPDATE")),
123 NodeEnum::DeleteStmt(stmt) => Some((stmt.relation.as_ref()?.relname.clone(), "DELETE")),
124 NodeEnum::InsertStmt(stmt) => {
125 let select = select_of(stmt.select_stmt.as_deref())?;
126 let table = stmt.relation.as_ref()?.relname.clone();
127 if table == PROJECTION_BACKLOG && is_keys_only(select) {
128 return None;
129 }
130 Some((table, "INSERT … SELECT"))
131 },
132 NodeEnum::SelectStmt(select) => {
133 refresh_call(select).map(|name| (name, "SELECT refresh_*()"))
134 },
135 _ => None,
136 }
137}
138
139fn select_of(node: Option<&Node>) -> Option<&protobuf::SelectStmt> {
142 match node?.node.as_ref()? {
143 NodeEnum::SelectStmt(select) if select.values_lists.is_empty() => Some(select),
144 _ => None,
145 }
146}
147
148fn is_keys_only(select: &protobuf::SelectStmt) -> bool {
154 let single_relation = select.from_clause.len() == 1
155 && matches!(
156 select.from_clause[0].node.as_ref(),
157 Some(NodeEnum::RangeVar(_))
158 );
159 single_relation
160 && select.target_list.len() == 2
161 && select.group_clause.is_empty()
162 && select.having_clause.is_none()
163 && select.with_clause.is_none()
164}
165
166fn refresh_call(select: &protobuf::SelectStmt) -> Option<String> {
167 select.target_list.iter().find_map(|target| {
168 let NodeEnum::ResTarget(res) = target.node.as_ref()? else {
169 return None;
170 };
171 let NodeEnum::FuncCall(call) = res.val.as_ref()?.node.as_ref()? else {
172 return None;
173 };
174 let name = call.funcname.last().and_then(|n| match n.node.as_ref()? {
175 NodeEnum::String(s) => Some(s.sval.clone()),
176 _ => None,
177 })?;
178 name.starts_with("refresh_").then_some(name)
179 })
180}