uqa_sql/schema/constraints/
references.rs1use crate::{ast::Expr, SQLError};
10use std::collections::BTreeSet;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum CheckReference<'a> {
14 WholeRow,
15 Column(&'a str),
16}
17
18impl<'a> CheckReference<'a> {
19 #[must_use]
21 pub const fn column_name(self) -> Option<&'a str> {
22 match self {
23 Self::WholeRow => None,
24 Self::Column(name) => Some(name),
25 }
26 }
27
28 pub fn attribute_number(
30 self,
31 lookup: impl FnOnce(&str) -> Result<i16, SQLError>,
32 ) -> Result<i16, SQLError> {
33 match self {
34 Self::WholeRow => Ok(0),
35 Self::Column("tableoid") => Ok(-6),
36 Self::Column(name) => lookup(name),
37 }
38 }
39}
40
41#[must_use]
43pub fn check_references(expression: &Expr) -> Vec<CheckReference<'_>> {
44 let mut references = References::default();
45 collect_references(expression, &mut references);
46 references.ordered
47}
48
49#[derive(Default)]
50struct References<'a> {
51 seen: BTreeSet<Option<&'a str>>,
52 ordered: Vec<CheckReference<'a>>,
53}
54
55impl<'a> References<'a> {
56 fn insert(&mut self, reference: CheckReference<'a>) {
57 if self.seen.insert(reference.column_name()) {
58 self.ordered.push(reference);
59 }
60 }
61}
62
63#[expect(
64 clippy::too_many_lines,
65 reason = "one traversal covers every constraint expression child"
66)]
67fn collect_references<'a>(expression: &'a crate::ast::Expr, output: &mut References<'a>) {
68 use crate::ast::{Expr, FrameBound};
69 match expression {
70 Expr::Column(name) | Expr::QualifiedColumn { column: name, .. } => {
71 output.insert(CheckReference::Column(name));
72 }
73 Expr::Func {
74 args,
75 order_by,
76 filter,
77 ..
78 } => {
79 for argument in args {
80 collect_references(argument, output);
81 }
82 for order in order_by {
83 collect_references(&order.expr, output);
84 }
85 if let Some(filter) = filter {
86 collect_references(filter, output);
87 }
88 }
89 Expr::Array(items)
90 | Expr::Row(items)
91 | Expr::CompositeRow { items, .. }
92 | Expr::And(items)
93 | Expr::Or(items) => {
94 for item in items {
95 collect_references(item, output);
96 }
97 }
98 Expr::Binary { lhs, rhs, .. } => {
99 collect_references(lhs, output);
100 collect_references(rhs, output);
101 }
102 Expr::Not(inner)
103 | Expr::UnaryMinus(inner)
104 | Expr::IsNull { expr: inner, .. }
105 | Expr::Cast { expr: inner, .. } => {
106 collect_references(inner, output);
107 }
108 Expr::Between { expr, low, high } => {
109 collect_references(expr, output);
110 collect_references(low, output);
111 collect_references(high, output);
112 }
113 Expr::InList { expr, list, .. } => {
114 collect_references(expr, output);
115 for item in list {
116 collect_references(item, output);
117 }
118 }
119 Expr::WindowCall {
120 args, spec, filter, ..
121 } => {
122 for expression in args
123 .iter()
124 .chain(filter.as_deref())
125 .chain(&spec.partition_by)
126 .chain(spec.order_by.iter().map(|order| &order.expr))
127 {
128 collect_references(expression, output);
129 }
130 for bound in spec
131 .frame
132 .iter()
133 .flat_map(|frame| [&frame.start, &frame.end])
134 {
135 if let FrameBound::Preceding(expression) | FrameBound::Following(expression) = bound
136 {
137 collect_references(expression, output);
138 }
139 }
140 }
141 Expr::Case {
142 base,
143 when,
144 else_branch,
145 } => {
146 if let Some(base) = base {
147 collect_references(base, output);
148 }
149 for (condition, result) in when {
150 collect_references(condition, output);
151 collect_references(result, output);
152 }
153 if let Some(else_branch) = else_branch {
154 collect_references(else_branch, output);
155 }
156 }
157 Expr::InSubquery { expr, .. } => collect_references(expr, output),
158 Expr::Star | Expr::QualifiedStar(_) => {
159 output.insert(CheckReference::WholeRow);
160 }
161 Expr::Default
162 | Expr::InternalColumn(_)
163 | Expr::Literal(_)
164 | Expr::TypedLiteral { .. }
165 | Expr::Param(_)
166 | Expr::ScalarSubquery(_)
167 | Expr::Exists { .. } => {}
168 }
169}