1use std::collections::HashSet;
12
13use datafusion::sql::sqlparser::ast::{Expr, Query, Statement, visit_expressions, visit_relations};
14use datafusion::sql::sqlparser::dialect::GenericDialect;
15use datafusion::sql::sqlparser::parser::Parser;
16
17use crate::{SqlError, SqlResult};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum SubqueryKind {
24 InSubquery,
26 NotInSubquery,
28 Exists,
30 NotExists,
32 Scalar,
35}
36
37#[derive(Debug, Clone)]
39pub struct DetectedSubquery {
40 pub kind: SubqueryKind,
41 pub inner_query: String,
43}
44
45pub fn detect_subqueries(sql: &str) -> SqlResult<Vec<DetectedSubquery>> {
52 let dialect = GenericDialect {};
53 let stmts = Parser::parse_sql(&dialect, sql).map_err(|e| SqlError::Unsupported {
54 feature: format!("subquery detection: parse error: {e}"),
55 })?;
56
57 let mut found = Vec::new();
58
59 for stmt in &stmts {
60 collect_subqueries(stmt, &mut found);
61 }
62
63 Ok(found)
64}
65
66fn collect_subqueries<V>(node: &V, out: &mut Vec<DetectedSubquery>)
80where
81 V: datafusion::sql::sqlparser::ast::Visit,
82{
83 let _ = visit_expressions(node, |expr| {
84 match expr {
85 Expr::InSubquery {
86 subquery, negated, ..
87 } => out.push(DetectedSubquery {
88 kind: if *negated {
89 SubqueryKind::NotInSubquery
90 } else {
91 SubqueryKind::InSubquery
92 },
93 inner_query: subquery.to_string(),
94 }),
95 Expr::Exists { subquery, negated } => out.push(DetectedSubquery {
96 kind: if *negated {
97 SubqueryKind::NotExists
98 } else {
99 SubqueryKind::Exists
100 },
101 inner_query: subquery.to_string(),
102 }),
103 Expr::Subquery(q) => out.push(DetectedSubquery {
104 kind: SubqueryKind::Scalar,
105 inner_query: q.to_string(),
106 }),
107 _ => {}
108 }
109 std::ops::ControlFlow::<()>::Continue(())
110 });
111}
112
113pub fn validate_no_streaming_subqueries(
125 sql: &str,
126 streaming_tables: &HashSet<String>,
127) -> SqlResult<()> {
128 if streaming_tables.is_empty() {
129 return Ok(());
130 }
131
132 let lower_tables: HashSet<String> = streaming_tables.iter().map(|s| s.to_lowercase()).collect();
135
136 let dialect = GenericDialect {};
137 let stmts = match Parser::parse_sql(&dialect, sql) {
138 Ok(s) => s,
139 Err(_) => return Ok(()), };
141
142 for stmt in &stmts {
143 {
144 let mut subqueries = Vec::new();
145 collect_subqueries(stmt, &mut subqueries);
146 for sq in &subqueries {
147 let inner_stmts =
148 Parser::parse_sql(&GenericDialect {}, &sq.inner_query).unwrap_or_default();
149 for s in &inner_stmts {
150 if let Statement::Query(iq) = s {
151 let names = extract_table_names_from_query(iq);
152 if names.iter().any(|t| lower_tables.contains(t)) {
153 return Err(SqlError::Unsupported {
154 feature: "correlated subquery over a streaming (unbounded) table \
155 is not supported; use a streaming join or MATCH_RECOGNIZE \
156 for event-pattern matching"
157 .into(),
158 });
159 }
160 }
161 }
162 }
163 }
164 }
165 Ok(())
166}
167
168fn extract_table_names_from_query(query: &Query) -> HashSet<String> {
169 let mut names = HashSet::new();
170 let _ = visit_relations(query, |relation| {
171 names.insert(relation.to_string().to_lowercase());
172 std::ops::ControlFlow::<()>::Continue(())
173 });
174 names
175}
176
177pub fn explain_subqueries(sql: &str) -> Option<String> {
183 let found = detect_subqueries(sql).unwrap_or_default();
184 if found.is_empty() {
185 return None;
186 }
187 let summary = found
188 .iter()
189 .map(|sq| match sq.kind {
190 SubqueryKind::InSubquery => "IN-subquery → semi-join",
191 SubqueryKind::NotInSubquery => "NOT IN-subquery → anti-join",
192 SubqueryKind::Exists => "EXISTS → semi-join",
193 SubqueryKind::NotExists => "NOT EXISTS → anti-join",
194 SubqueryKind::Scalar => "scalar subquery → cross-apply",
195 })
196 .collect::<Vec<_>>()
197 .join(", ");
198 Some(format!("subqueries: [{summary}]"))
199}
200
201#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn detects_in_subquery() {
209 let sql = "SELECT * FROM orders WHERE customer_id IN (SELECT id FROM vip_customers)";
210 let found = detect_subqueries(sql).unwrap();
211 assert_eq!(found.len(), 1);
212 assert_eq!(found[0].kind, SubqueryKind::InSubquery);
213 }
214
215 #[test]
216 fn detects_not_in_subquery() {
217 let sql = "SELECT * FROM orders WHERE customer_id NOT IN (SELECT id FROM banned)";
218 let found = detect_subqueries(sql).unwrap();
219 assert_eq!(found.len(), 1);
220 assert_eq!(found[0].kind, SubqueryKind::NotInSubquery);
221 }
222
223 #[test]
224 fn detects_exists_subquery() {
225 let sql = "SELECT * FROM orders o WHERE EXISTS (SELECT 1 FROM payments p WHERE p.order_id = o.id)";
226 let found = detect_subqueries(sql).unwrap();
227 assert_eq!(found.len(), 1);
228 assert_eq!(found[0].kind, SubqueryKind::Exists);
229 }
230
231 #[test]
232 fn detects_not_exists_subquery() {
233 let sql = "SELECT * FROM orders o WHERE NOT EXISTS (SELECT 1 FROM payments p WHERE p.order_id = o.id)";
234 let found = detect_subqueries(sql).unwrap();
235 assert_eq!(found.len(), 1);
236 assert_eq!(found[0].kind, SubqueryKind::NotExists);
237 }
238
239 #[test]
240 fn detects_scalar_subquery() {
241 let sql = "SELECT id, (SELECT MAX(amount) FROM payments WHERE order_id = o.id) as max_payment FROM orders o";
242 let found = detect_subqueries(sql).unwrap();
243 assert_eq!(found.len(), 1);
244 assert_eq!(found[0].kind, SubqueryKind::Scalar);
245 }
246
247 #[test]
248 fn detects_nested_subqueries() {
249 let sql = "SELECT * FROM a WHERE x IN (SELECT y FROM b WHERE y NOT IN (SELECT z FROM c))";
250 let found = detect_subqueries(sql).unwrap();
251 assert!(found.len() >= 2);
252 assert!(found.iter().any(|s| s.kind == SubqueryKind::InSubquery));
253 assert!(found.iter().any(|s| s.kind == SubqueryKind::NotInSubquery));
254 }
255
256 #[test]
257 fn no_subqueries_returns_empty() {
258 let sql = "SELECT id, amount FROM orders WHERE status = 'completed'";
259 let found = detect_subqueries(sql).unwrap();
260 assert!(found.is_empty());
261 }
262
263 #[test]
264 fn streaming_guard_passes_when_no_streaming_tables() {
265 let sql = "SELECT * FROM t WHERE id IN (SELECT id FROM s)";
266 let streaming: HashSet<String> = HashSet::new();
267 assert!(validate_no_streaming_subqueries(sql, &streaming).is_ok());
268 }
269
270 #[test]
271 fn streaming_guard_rejects_subquery_over_streaming_table() {
272 let sql = "SELECT * FROM events WHERE id IN (SELECT id FROM live_stream)";
273 let mut streaming = HashSet::new();
274 streaming.insert("live_stream".into());
275 let err = validate_no_streaming_subqueries(sql, &streaming).unwrap_err();
276 assert!(matches!(err, SqlError::Unsupported { .. }));
277 }
278
279 #[test]
280 fn streaming_guard_passes_for_batch_tables() {
281 let sql = "SELECT * FROM events WHERE id IN (SELECT id FROM reference_table)";
282 let mut streaming = HashSet::new();
283 streaming.insert("live_stream".into());
284 assert!(validate_no_streaming_subqueries(sql, &streaming).is_ok());
285 }
286
287 #[test]
288 fn explain_subqueries_returns_none_for_plain_sql() {
289 assert!(explain_subqueries("SELECT 1").is_none());
290 }
291
292 #[test]
293 fn explain_subqueries_describes_kinds() {
294 let sql = "SELECT * FROM t WHERE x IN (SELECT y FROM s)";
295 let desc = explain_subqueries(sql).unwrap();
296 assert!(desc.contains("semi-join"));
297 }
298
299 #[test]
300 fn case_expression_does_not_panic() {
301 let sql = "SELECT CASE WHEN x > 0 THEN 'pos' ELSE 'neg' END FROM t";
302 let found = detect_subqueries(sql).unwrap();
303 assert!(found.is_empty());
304 }
305
306 #[test]
316 fn finds_a_subquery_in_a_union_branch() {
317 let sql = "SELECT a FROM t WHERE a IN (SELECT id FROM s) UNION ALL SELECT b FROM u";
318 let found = detect_subqueries(sql).unwrap();
319 assert_eq!(found.len(), 1, "union branch not scanned: {found:?}");
320 assert_eq!(found[0].kind, SubqueryKind::InSubquery);
321 }
322
323 #[test]
325 fn finds_a_subquery_inside_a_cte() {
326 let sql = "WITH c AS (SELECT id FROM t WHERE id IN (SELECT id FROM s)) SELECT * FROM c";
327 let found = detect_subqueries(sql).unwrap();
328 assert_eq!(found.len(), 1, "CTE body not scanned: {found:?}");
329 }
330
331 #[test]
333 fn finds_a_subquery_in_a_join_condition() {
334 let sql = "SELECT * FROM a JOIN b ON b.id IN (SELECT id FROM s)";
335 let found = detect_subqueries(sql).unwrap();
336 assert_eq!(found.len(), 1, "join condition not scanned: {found:?}");
337 }
338
339 #[test]
341 fn finds_a_subquery_inside_a_derived_table() {
342 let sql = "SELECT * FROM (SELECT id FROM t WHERE id IN (SELECT id FROM s)) d";
343 let found = detect_subqueries(sql).unwrap();
344 assert_eq!(found.len(), 1, "derived table not scanned: {found:?}");
345 }
346
347 #[test]
351 fn finds_a_subquery_behind_parentheses() {
352 let sql = "SELECT * FROM t WHERE (id IN (SELECT id FROM s))";
353 let found = detect_subqueries(sql).unwrap();
354 assert_eq!(found.len(), 1, "parenthesised predicate not scanned: {found:?}");
355 }
356
357 #[test]
360 fn the_streaming_guard_is_not_bypassed_by_a_union() {
361 let sql = "SELECT a FROM events WHERE a IN (SELECT id FROM live_stream) \
362 UNION ALL SELECT b FROM other";
363 let mut streaming = HashSet::new();
364 streaming.insert("live_stream".into());
365 assert!(
366 validate_no_streaming_subqueries(sql, &streaming).is_err(),
367 "a streaming subquery in a UNION branch slipped past the guard"
368 );
369 }
370
371 #[test]
373 fn the_streaming_guard_is_not_bypassed_by_a_cte() {
374 let sql = "WITH c AS (SELECT id FROM events WHERE id IN (SELECT id FROM live_stream)) \
375 SELECT * FROM c";
376 let mut streaming = HashSet::new();
377 streaming.insert("live_stream".into());
378 assert!(
379 validate_no_streaming_subqueries(sql, &streaming).is_err(),
380 "a streaming subquery in a CTE slipped past the guard"
381 );
382 }
383
384 #[test]
387 fn the_streaming_guard_examines_insert_select() {
388 let sql = "INSERT INTO sink SELECT id FROM events WHERE id IN (SELECT id FROM live_stream)";
389 let mut streaming = HashSet::new();
390 streaming.insert("live_stream".into());
391 assert!(
392 validate_no_streaming_subqueries(sql, &streaming).is_err(),
393 "INSERT ... SELECT was not examined"
394 );
395 }
396}