1use std::sync::Arc;
24
25use radixdb_sql::ast::Expression;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum LogicalRelationKind {
29 Table,
30 Derived,
31 Cte,
32 Values,
33 Function,
34 Opaque,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct LogicalRelation {
39 pub ordinal: usize,
40 pub visible_name: Option<String>,
41 pub kind: LogicalRelationKind,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum JoinReorderBarrier {
46 ReorderableInner,
47 Cross,
48 Left,
49 Right,
50 Full,
51 NaturalOrUsing,
52 Other,
53}
54
55impl JoinReorderBarrier {
56 pub const fn permits_inner_reorder(self) -> bool {
57 matches!(self, Self::ReorderableInner)
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct LogicalJoinEdge {
63 pub ordinal: usize,
64 pub left_relations: Arc<[usize]>,
65 pub right_relations: Arc<[usize]>,
66 pub barrier: JoinReorderBarrier,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct LogicalJoinGraph {
71 pub relations: Arc<[LogicalRelation]>,
72 pub edges: Arc<[LogicalJoinEdge]>,
73}
74
75impl LogicalJoinGraph {
76 pub fn bind(table_expression: &Expression) -> Option<Self> {
77 if !matches!(table_expression, Expression::JoinSource(_)) {
78 return None;
79 }
80 let mut builder = LogicalJoinGraphBuilder::default();
81 builder.bind_subtree(table_expression);
82 Some(Self {
83 relations: Arc::from(builder.relations),
84 edges: Arc::from(builder.edges),
85 })
86 }
87
88 pub fn reorderable_edge_count(&self) -> usize {
89 self.edges
90 .iter()
91 .filter(|edge| edge.barrier.permits_inner_reorder())
92 .count()
93 }
94}
95
96#[derive(Default)]
97struct LogicalJoinGraphBuilder {
98 relations: Vec<LogicalRelation>,
99 edges: Vec<LogicalJoinEdge>,
100}
101
102impl LogicalJoinGraphBuilder {
103 fn bind_subtree(&mut self, expression: &Expression) -> Vec<usize> {
104 if let Expression::JoinSource(join) = expression {
105 let left_relations = self.bind_subtree(&join.left);
106 let right_relations = self.bind_subtree(&join.right);
107 self.edges.push(LogicalJoinEdge {
108 ordinal: self.edges.len(),
109 left_relations: Arc::from(left_relations.clone()),
110 right_relations: Arc::from(right_relations.clone()),
111 barrier: classify_barrier(join),
112 });
113 let mut relations = left_relations;
114 relations.extend(right_relations);
115 relations
116 } else {
117 let ordinal = self.relations.len();
118 let (visible_name, kind) = classify_relation(expression);
119 self.relations.push(LogicalRelation {
120 ordinal,
121 visible_name,
122 kind,
123 });
124 vec![ordinal]
125 }
126 }
127}
128
129fn classify_barrier(join: &radixdb_sql::ast::JoinTableSource) -> JoinReorderBarrier {
130 if !join.using_columns.is_empty() || join.join_type.to_uppercase().contains("NATURAL") {
131 return JoinReorderBarrier::NaturalOrUsing;
132 }
133 match join.join_type.trim().to_uppercase().as_str() {
134 "INNER" if join.condition.is_some() => JoinReorderBarrier::ReorderableInner,
135 "CROSS" => JoinReorderBarrier::Cross,
136 "LEFT" | "LEFT OUTER" => JoinReorderBarrier::Left,
137 "RIGHT" | "RIGHT OUTER" => JoinReorderBarrier::Right,
138 "FULL" | "FULL OUTER" => JoinReorderBarrier::Full,
139 _ => JoinReorderBarrier::Other,
140 }
141}
142
143fn classify_relation(expression: &Expression) -> (Option<String>, LogicalRelationKind) {
144 match expression {
145 Expression::TableSource(source) => (
146 Some(
147 source
148 .alias
149 .as_ref()
150 .unwrap_or(&source.name)
151 .value_lower()
152 .to_string(),
153 ),
154 LogicalRelationKind::Table,
155 ),
156 Expression::SubquerySource(source) => (
157 source
158 .alias
159 .as_ref()
160 .map(|alias| alias.value_lower().to_string()),
161 LogicalRelationKind::Derived,
162 ),
163 Expression::CteReference(source) => (
164 Some(
165 source
166 .alias
167 .as_ref()
168 .unwrap_or(&source.name)
169 .value_lower()
170 .to_string(),
171 ),
172 LogicalRelationKind::Cte,
173 ),
174 Expression::ValuesSource(source) => (
175 source
176 .alias
177 .as_ref()
178 .map(|alias| alias.value_lower().to_string()),
179 LogicalRelationKind::Values,
180 ),
181 Expression::FunctionTableSource(source) => (
182 Some(
183 source
184 .alias
185 .as_ref()
186 .unwrap_or(&source.function)
187 .value_lower()
188 .to_string(),
189 ),
190 LogicalRelationKind::Function,
191 ),
192 Expression::Aliased(source) => (
193 Some(source.alias.value_lower().to_string()),
194 LogicalRelationKind::Derived,
195 ),
196 _ => (None, LogicalRelationKind::Opaque),
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use radixdb_sql::Statement;
204
205 fn graph(sql: &str) -> LogicalJoinGraph {
206 let mut statements = radixdb_sql::parse_sql(sql).unwrap();
207 let Statement::Select(statement) = statements.remove(0) else {
208 panic!("expected SELECT")
209 };
210 LogicalJoinGraph::bind(statement.table_expr.as_deref().unwrap()).unwrap()
211 }
212
213 #[test]
214 fn relation_sets_and_outer_barriers_are_explicit() {
215 let graph = graph(
216 "SELECT a.id FROM a \
217 INNER JOIN b ON b.a_id = a.id \
218 LEFT JOIN (SELECT id FROM c) c1 ON c1.id = b.c_id \
219 INNER JOIN d ON d.id = a.d_id",
220 );
221
222 assert_eq!(graph.relations.len(), 4);
223 assert_eq!(graph.relations[0].visible_name.as_deref(), Some("a"));
224 assert_eq!(graph.relations[2].visible_name.as_deref(), Some("c1"));
225 assert_eq!(graph.relations[2].kind, LogicalRelationKind::Derived);
226 assert_eq!(graph.edges.len(), 3);
227 assert_eq!(graph.edges[0].left_relations.as_ref(), &[0]);
228 assert_eq!(graph.edges[0].right_relations.as_ref(), &[1]);
229 assert_eq!(graph.edges[0].barrier, JoinReorderBarrier::ReorderableInner);
230 assert_eq!(graph.edges[1].left_relations.as_ref(), &[0, 1]);
231 assert_eq!(graph.edges[1].right_relations.as_ref(), &[2]);
232 assert_eq!(graph.edges[1].barrier, JoinReorderBarrier::Left);
233 assert_eq!(graph.edges[2].left_relations.as_ref(), &[0, 1, 2]);
234 assert_eq!(graph.edges[2].right_relations.as_ref(), &[3]);
235 assert_eq!(graph.reorderable_edge_count(), 2);
236 }
237
238 #[test]
239 fn using_and_cross_edges_are_not_silently_reorderable() {
240 let graph = graph("SELECT * FROM a JOIN b USING (id) CROSS JOIN c");
241 assert_eq!(graph.edges[0].barrier, JoinReorderBarrier::NaturalOrUsing);
242 assert_eq!(graph.edges[1].barrier, JoinReorderBarrier::Cross);
243 assert_eq!(graph.reorderable_edge_count(), 0);
244 }
245}