krishiv_sql/
statement_completion.rs1#![forbid(unsafe_code)]
2use datafusion::prelude::SessionContext;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct UseTarget {
25 pub catalog: Option<String>,
27 pub schema: Option<String>,
29}
30
31fn unquote(ident: &str) -> String {
32 let t = ident.trim();
33 for q in ['`', '"'] {
34 if let Some(inner) = t.strip_prefix(q).and_then(|s| s.strip_suffix(q)) {
35 return inner.to_string();
36 }
37 }
38 t.to_string()
39}
40
41pub fn parse_use(query: &str) -> Option<UseTarget> {
49 let q = query.trim().trim_end_matches(';').trim();
50 let (head, remainder) = q.split_once(char::is_whitespace)?;
52 if !head.eq_ignore_ascii_case("USE") {
53 return None;
54 }
55 let remainder = remainder.trim();
56 if remainder.is_empty() {
57 return None;
58 }
59 let (is_catalog, name_part) = match remainder.split_once(char::is_whitespace) {
63 Some((w, r)) if w.eq_ignore_ascii_case("CATALOG") => (true, r.trim()),
64 Some((w, r))
65 if w.eq_ignore_ascii_case("SCHEMA")
66 || w.eq_ignore_ascii_case("DATABASE")
67 || w.eq_ignore_ascii_case("NAMESPACE") =>
68 {
69 (false, r.trim())
70 }
71 _ => (false, remainder),
72 };
73 let name = unquote(name_part);
74 if name.is_empty() {
75 return None;
76 }
77 if is_catalog {
78 return Some(UseTarget {
79 catalog: Some(name),
80 schema: None,
81 });
82 }
83 if let Some((cat, sch)) = name.split_once('.') {
85 return Some(UseTarget {
86 catalog: Some(unquote(cat)),
87 schema: Some(unquote(sch)),
88 });
89 }
90 Some(UseTarget {
91 catalog: None,
92 schema: Some(name),
93 })
94}
95
96pub fn apply_use(ctx: &SessionContext, query: &str) -> Option<Result<(), String>> {
100 let target = parse_use(query)?;
101 let state_ref = ctx.state_ref();
102 let mut state = state_ref.write();
103 let opts = state.config_mut().options_mut();
104 if let Some(catalog) = target.catalog {
105 opts.catalog.default_catalog = catalog;
106 }
107 if let Some(schema) = target.schema {
108 opts.catalog.default_schema = schema;
109 }
110 Some(Ok(()))
111}
112
113pub fn rewrite_show_databases(query: &str) -> Option<String> {
117 let q = query.trim().trim_end_matches(';').trim();
118 let upper = q.to_ascii_uppercase();
119 let is_show = upper.starts_with("SHOW DATABASES") || upper.starts_with("SHOW SCHEMAS");
120 if !is_show {
121 return None;
122 }
123 let like_clause = if let Some(idx) = upper.find(" LIKE ") {
125 let pat = q[idx + 6..].trim().trim_end_matches(';').trim();
126 Some(format!(" WHERE schema_name LIKE {pat}"))
127 } else {
128 None
129 };
130 Some(format!(
131 "SELECT schema_name AS namespace FROM information_schema.schemata{} ORDER BY namespace",
132 like_clause.unwrap_or_default()
133 ))
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn parse_use_forms() {
142 assert_eq!(
143 parse_use("USE analytics"),
144 Some(UseTarget {
145 catalog: None,
146 schema: Some("analytics".into())
147 })
148 );
149 assert_eq!(
150 parse_use("USE SCHEMA sales"),
151 Some(UseTarget {
152 catalog: None,
153 schema: Some("sales".into())
154 })
155 );
156 assert_eq!(
157 parse_use("USE DATABASE sales;"),
158 Some(UseTarget {
159 catalog: None,
160 schema: Some("sales".into())
161 })
162 );
163 assert_eq!(
164 parse_use("USE CATALOG lakehouse"),
165 Some(UseTarget {
166 catalog: Some("lakehouse".into()),
167 schema: None
168 })
169 );
170 assert_eq!(
171 parse_use("USE lake.sales"),
172 Some(UseTarget {
173 catalog: Some("lake".into()),
174 schema: Some("sales".into())
175 })
176 );
177 assert_eq!(
178 parse_use("USE `my schema`").unwrap().schema.as_deref(),
179 Some("my schema")
180 );
181 assert_eq!(parse_use("SELECT 1"), None);
182 }
183
184 #[test]
185 fn show_databases_rewrite() {
186 assert!(
187 rewrite_show_databases("SHOW DATABASES")
188 .unwrap()
189 .contains("information_schema.schemata")
190 );
191 assert!(
192 rewrite_show_databases("SHOW SCHEMAS")
193 .unwrap()
194 .contains("AS namespace")
195 );
196 let with_like = rewrite_show_databases("SHOW DATABASES LIKE 'sal%'").unwrap();
197 assert!(with_like.contains("LIKE 'sal%'"));
198 assert_eq!(rewrite_show_databases("SHOW TABLES"), None);
199 }
200
201 #[tokio::test]
202 async fn use_changes_default_schema_end_to_end() {
203 let engine = crate::SqlEngine::new();
204 engine
207 .sql("USE information_schema")
208 .await
209 .expect("USE runs");
210 let batches = engine
211 .sql("SELECT count(*) AS c FROM tables")
212 .await
213 .expect("unqualified `tables` resolves via the new default schema")
214 .collect()
215 .await
216 .expect("collect");
217 let total: i64 = {
218 use arrow::array::Int64Array;
219 batches[0]
220 .column(0)
221 .as_any()
222 .downcast_ref::<Int64Array>()
223 .unwrap()
224 .value(0)
225 };
226 assert!(total > 0, "information_schema.tables should be non-empty");
227 }
228
229 #[tokio::test]
230 async fn show_databases_lists_schemas() {
231 let engine = crate::SqlEngine::new();
232 let batches = engine
233 .sql("SHOW DATABASES")
234 .await
235 .expect("SHOW DATABASES runs")
236 .collect()
237 .await
238 .expect("collect");
239 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
241 assert!(total >= 1);
242 assert_eq!(batches[0].schema().field(0).name(), "namespace");
243 }
244}