1use std::borrow::Cow;
22use std::cell::Cell;
23
24use crate::highlighter::{Color, NoSyntaxHighlighter, SyntaxHighlighter};
25
26use datafusion::sql::parser::{DFParser, Statement};
27use datafusion::sql::sqlparser::dialect::dialect_from_str;
28use datafusion_common::config::Dialect;
29
30use rustyline::completion::{Completer, FilenameCompleter, Pair};
31use rustyline::error::ReadlineError;
32use rustyline::highlight::{CmdKind, Highlighter};
33use rustyline::hint::Hinter;
34use rustyline::validate::{ValidationContext, ValidationResult, Validator};
35use rustyline::{Context, Helper, Result};
36
37const DEFAULT_HINT_SUGGESTION: &str = " \\? for help, \\q to quit";
39
40pub struct CliHelper {
41 completer: FilenameCompleter,
42 dialect: Dialect,
43 highlighter: Box<dyn Highlighter>,
44 show_hint: Cell<bool>,
48}
49
50impl CliHelper {
51 pub fn new(dialect: &Dialect, color: bool) -> Self {
52 let highlighter: Box<dyn Highlighter> = if !color {
53 Box::new(NoSyntaxHighlighter {})
54 } else {
55 Box::new(SyntaxHighlighter::new(dialect))
56 };
57 Self {
58 completer: FilenameCompleter::new(),
59 dialect: *dialect,
60 highlighter,
61 show_hint: Cell::new(true),
62 }
63 }
64
65 pub fn set_dialect(&mut self, dialect: &Dialect) {
66 if *dialect != self.dialect {
67 self.dialect = *dialect;
68 }
69 }
70
71 pub fn reset_hint(&self) {
73 self.show_hint.set(true);
74 }
75
76 fn validate_input(&self, input: &str) -> Result<ValidationResult> {
77 if let Some(sql) = input.strip_suffix(';') {
78 let dialect = match dialect_from_str(self.dialect) {
79 Some(dialect) => dialect,
80 None => {
81 return Ok(ValidationResult::Invalid(Some(format!(
82 " 🤔 Invalid dialect: {}",
83 self.dialect
84 ))));
85 }
86 };
87 let lines = split_from_semicolon(sql);
88 for line in lines {
89 match DFParser::parse_sql_with_dialect(&line, dialect.as_ref()) {
90 Ok(statements) if statements.is_empty() => {
91 return Ok(ValidationResult::Invalid(Some(
92 " 🤔 You entered an empty statement".to_string(),
93 )));
94 }
95 Ok(_statements) => {}
96 Err(err) => {
97 return Ok(ValidationResult::Invalid(Some(format!(
98 " 🤔 Invalid statement: {err}",
99 ))));
100 }
101 }
102 }
103 Ok(ValidationResult::Valid(None))
104 } else if input.starts_with('\\') {
105 Ok(ValidationResult::Valid(None))
107 } else {
108 Ok(ValidationResult::Incomplete)
109 }
110 }
111}
112
113impl Default for CliHelper {
114 fn default() -> Self {
115 Self::new(&Dialect::Generic, false)
116 }
117}
118
119impl Highlighter for CliHelper {
120 fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
121 self.highlighter.highlight(line, pos)
122 }
123
124 fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool {
125 self.highlighter.highlight_char(line, pos, kind)
126 }
127
128 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
129 Color::gray(hint).into()
130 }
131}
132
133impl Hinter for CliHelper {
134 type Hint = String;
135
136 fn hint(&self, line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<String> {
137 if !line.is_empty() {
138 self.show_hint.set(false);
139 }
140 (self.show_hint.get() && line.trim().is_empty())
141 .then(|| DEFAULT_HINT_SUGGESTION.to_owned())
142 }
143}
144
145fn is_open_quote_for_location(line: &str, pos: usize) -> bool {
148 let mut sql = line[..pos].to_string();
149 sql.push('\'');
150 DFParser::parse_sql(&sql).is_ok_and(|stmts| {
151 matches!(stmts.back(), Some(Statement::CreateExternalTable(_)))
152 })
153}
154
155impl Completer for CliHelper {
156 type Candidate = Pair;
157
158 fn complete(
159 &self,
160 line: &str,
161 pos: usize,
162 ctx: &Context<'_>,
163 ) -> std::result::Result<(usize, Vec<Pair>), ReadlineError> {
164 if is_open_quote_for_location(line, pos) {
165 self.completer.complete(line, pos, ctx)
166 } else {
167 Ok((0, Vec::with_capacity(0)))
168 }
169 }
170}
171
172impl Validator for CliHelper {
173 fn validate(&self, ctx: &mut ValidationContext<'_>) -> Result<ValidationResult> {
174 let input = ctx.input().trim_end();
175 let result = self.validate_input(input);
176 self.reset_hint();
177 result
178 }
179}
180
181impl Helper for CliHelper {}
182
183pub(crate) fn split_from_semicolon(sql: &str) -> Vec<String> {
185 let mut commands = Vec::new();
186 let mut current_command = String::new();
187 let mut in_single_quote = false;
188 let mut in_double_quote = false;
189
190 for c in sql.chars() {
191 if c == '\'' && !in_double_quote {
192 in_single_quote = !in_single_quote;
193 } else if c == '"' && !in_single_quote {
194 in_double_quote = !in_double_quote;
195 }
196
197 if c == ';' && !in_single_quote && !in_double_quote {
198 if !current_command.trim().is_empty() {
199 commands.push(format!("{};", current_command.trim()));
200 current_command.clear();
201 }
202 } else {
203 current_command.push(c);
204 }
205 }
206
207 if !current_command.trim().is_empty() {
208 commands.push(format!("{};", current_command.trim()));
209 }
210
211 commands
212}
213
214#[cfg(test)]
215mod tests {
216 use std::io::{BufRead, Cursor};
217
218 use super::*;
219
220 fn readline_direct(
221 mut reader: impl BufRead,
222 validator: &CliHelper,
223 ) -> Result<ValidationResult> {
224 let mut input = String::new();
225
226 if reader.read_line(&mut input)? == 0 {
227 return Err(ReadlineError::Eof);
228 }
229
230 validator.validate_input(&input)
231 }
232
233 #[test]
234 fn unescape_readline_input() -> Result<()> {
235 let validator = CliHelper::default();
236
237 let result = readline_direct(
239 Cursor::new(
240 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' ',');"
241 .as_bytes(),
242 ),
243 &validator,
244 )?;
245 assert!(matches!(result, ValidationResult::Valid(None)));
246
247 let result = readline_direct(
248 Cursor::new(
249 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' '\0');"
250 .as_bytes()),
251 &validator,
252 )?;
253 assert!(matches!(result, ValidationResult::Valid(None)));
254
255 let result = readline_direct(
256 Cursor::new(
257 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' '\n');"
258 .as_bytes()),
259 &validator,
260 )?;
261 assert!(matches!(result, ValidationResult::Valid(None)));
262
263 let result = readline_direct(
264 Cursor::new(
265 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' '\r');"
266 .as_bytes()),
267 &validator,
268 )?;
269 assert!(matches!(result, ValidationResult::Valid(None)));
270
271 let result = readline_direct(
272 Cursor::new(
273 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' '\t');"
274 .as_bytes()),
275 &validator,
276 )?;
277 assert!(matches!(result, ValidationResult::Valid(None)));
278
279 let result = readline_direct(
280 Cursor::new(
281 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' '\\');"
282 .as_bytes()),
283 &validator,
284 )?;
285 assert!(matches!(result, ValidationResult::Valid(None)));
286
287 let result = readline_direct(
288 Cursor::new(
289 r"create external table test stored as csv location 'data.csv' options ('format.delimiter' ',,');"
290 .as_bytes()),
291 &validator,
292 )?;
293 assert!(matches!(result, ValidationResult::Valid(None)));
294
295 let result = readline_direct(
296 Cursor::new(
297 r"select '\', '\\', '\\\\\', 'dsdsds\\\\', '\t', '\0', '\n';".as_bytes(),
298 ),
299 &validator,
300 )?;
301 assert!(matches!(result, ValidationResult::Valid(None)));
302
303 Ok(())
304 }
305
306 #[test]
307 fn sql_dialect() -> Result<()> {
308 let mut validator = CliHelper::default();
309
310 let result =
312 readline_direct(Cursor::new(r"select 1 # 2;".as_bytes()), &validator)?;
313 assert!(
314 matches!(result, ValidationResult::Invalid(Some(e)) if e.contains("Invalid statement"))
315 );
316
317 validator.set_dialect(&Dialect::PostgreSQL);
319 let result =
320 readline_direct(Cursor::new(r"select 1 # 2;".as_bytes()), &validator)?;
321 assert!(matches!(result, ValidationResult::Valid(None)));
322
323 Ok(())
324 }
325
326 #[test]
327 fn test_split_from_semicolon() {
328 let sql = "SELECT 1; SELECT 2;";
329 let expected = vec!["SELECT 1;", "SELECT 2;"];
330 assert_eq!(split_from_semicolon(sql), expected);
331
332 let sql = r#"SELECT ";";"#;
333 let expected = vec![r#"SELECT ";";"#];
334 assert_eq!(split_from_semicolon(sql), expected);
335
336 let sql = "SELECT ';';";
337 let expected = vec!["SELECT ';';"];
338 assert_eq!(split_from_semicolon(sql), expected);
339
340 let sql = r#"SELECT 1; SELECT 'value;value'; SELECT 1 as "text;text";"#;
341 let expected = vec![
342 "SELECT 1;",
343 "SELECT 'value;value';",
344 r#"SELECT 1 as "text;text";"#,
345 ];
346 assert_eq!(split_from_semicolon(sql), expected);
347
348 let sql = "";
349 let expected: Vec<String> = Vec::new();
350 assert_eq!(split_from_semicolon(sql), expected);
351
352 let sql = "SELECT 1";
353 let expected = vec!["SELECT 1;"];
354 assert_eq!(split_from_semicolon(sql), expected);
355
356 let sql = "SELECT 1; ";
357 let expected = vec!["SELECT 1;"];
358 assert_eq!(split_from_semicolon(sql), expected);
359 }
360}