1use std::collections::HashMap;
32use std::ops::ControlFlow;
33use std::sync::Arc;
34
35use datafusion::catalog::default_table_source::provider_as_source;
36use datafusion::catalog::TableProvider;
37use datafusion::common::parsers::CompressionTypeVariant;
38use datafusion::error::{DataFusionError, Result};
39use datafusion::execution::context::SessionState;
40use datafusion::logical_expr::planner::{
41 PlannedRelation, RelationPlanner, RelationPlannerContext, RelationPlanning,
42};
43use datafusion::logical_expr::LogicalPlanBuilder;
44use datafusion::prelude::SessionConfig;
45use datafusion::sql::parser::{CopyToSource, Statement as DFStatement};
46use datafusion::sql::sqlparser::ast::{
47 BinaryOperator, Expr, FunctionArg, FunctionArgExpr, Ident, ObjectName, TableFactor,
48 TableFunctionArgs, Value, Visit, Visitor,
49};
50use parking_lot::Mutex;
51
52use crate::file_table::{open_files, FileFormat, FileOptions};
53use crate::graph::closest_name;
54
55const FUNCTIONS: [(&str, Function); 6] = [
57 ("read_parquet", Function::Parquet),
58 ("read_csv", Function::Csv),
59 ("read_csv_auto", Function::Csv),
60 ("read_json", Function::Json),
61 ("read_json_auto", Function::Json),
62 ("read_ndjson", Function::NdJson),
63];
64
65pub fn names() -> impl Iterator<Item = &'static str> {
67 FUNCTIONS.iter().map(|(name, _)| *name)
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71enum Function {
72 Parquet,
73 Csv,
74 Json,
75 NdJson,
77}
78
79impl Function {
80 fn lookup(name: &str) -> Option<Self> {
81 FUNCTIONS.iter().find(|(n, _)| *n == name).map(|(_, f)| *f)
82 }
83
84 fn options(self) -> &'static [&'static str] {
86 match self {
87 Function::Parquet => &[],
88 Function::Csv => &[
89 "header",
90 "delim",
91 "sep",
92 "delimiter",
93 "quote",
94 "compression",
95 ],
96 Function::Json => &["compression", "format"],
97 Function::NdJson => &["compression"],
98 }
99 }
100
101 fn format(self) -> FileFormat {
102 match self {
103 Function::Parquet => FileFormat::Parquet,
104 Function::Csv => FileFormat::Csv,
105 Function::Json | Function::NdJson => FileFormat::Json,
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112struct ReadCall {
113 locations: Vec<String>,
114 options: FileOptions,
115}
116
117#[derive(Debug, Default)]
120pub struct ReadTables {
121 tables: Mutex<HashMap<String, Shared>>,
122}
123
124type Shared = (Arc<dyn TableProvider>, usize);
126
127pub(crate) fn with_read_tables(config: SessionConfig) -> SessionConfig {
129 config.with_extension(Arc::new(ReadTables::default()))
130}
131
132fn read_tables(config: &SessionConfig) -> Result<Arc<ReadTables>> {
133 config
134 .get_extension::<ReadTables>()
135 .ok_or_else(|| DataFusionError::Internal("the session has no read_* tables".into()))
136}
137
138#[must_use = "the tables are forgotten when this is dropped"]
141pub struct OpenReads {
142 tables: Option<Arc<ReadTables>>,
143 keys: Vec<String>,
144}
145
146impl Drop for OpenReads {
147 fn drop(&mut self) {
148 let Some(tables) = &self.tables else {
149 return;
150 };
151 let mut tables = tables.tables.lock();
152 for key in &self.keys {
153 if let Some((_, users)) = tables.get_mut(key) {
154 *users -= 1;
155 if *users == 0 {
156 tables.remove(key);
157 }
158 }
159 }
160 }
161}
162
163pub async fn open_reads(state: &SessionState, statement: &DFStatement) -> Result<OpenReads> {
166 let mut calls = Calls::default();
167 collect(statement, &mut calls)?;
168 if calls.calls.is_empty() {
169 return Ok(OpenReads {
170 tables: None,
171 keys: Vec::new(),
172 });
173 }
174 let tables = read_tables(state.config())?;
175 let mut open = OpenReads {
176 tables: Some(Arc::clone(&tables)),
177 keys: Vec::new(),
178 };
179 for (key, call) in calls.calls {
180 let table = open_files(state, &call.locations, &call.options).await?;
181 let mut tables = tables.tables.lock();
182 tables
183 .entry(key.clone())
184 .and_modify(|(_, users)| *users += 1)
185 .or_insert((table, 1));
186 open.keys.push(key);
187 }
188 Ok(open)
189}
190
191#[derive(Default)]
193struct Calls {
194 calls: Vec<(String, ReadCall)>,
195}
196
197impl Visitor for Calls {
198 type Break = DataFusionError;
199
200 fn pre_visit_table_factor(&mut self, factor: &TableFactor) -> ControlFlow<Self::Break> {
201 let TableFactor::Table {
202 name,
203 args: Some(args),
204 ..
205 } = factor
206 else {
207 return ControlFlow::Continue(());
208 };
209 let Some(function) = function_name(name, |ident| default_normalize(ident.clone())) else {
210 return ControlFlow::Continue(());
211 };
212 let Some(f) = Function::lookup(&function) else {
213 return ControlFlow::Continue(());
214 };
215 let key = call_key(name, args);
216 if self.calls.iter().any(|(k, _)| *k == key) {
217 return ControlFlow::Continue(());
218 }
219 match parse_call(&function, f, args) {
220 Ok(call) => {
221 self.calls.push((key, call));
222 ControlFlow::Continue(())
223 }
224 Err(e) => ControlFlow::Break(e),
225 }
226 }
227}
228
229fn collect(statement: &DFStatement, calls: &mut Calls) -> Result<()> {
230 let flow = match statement {
231 DFStatement::Statement(s) => s.visit(calls),
232 DFStatement::CopyTo(copy) => match ©.source {
233 CopyToSource::Query(query) => query.visit(calls),
234 CopyToSource::Relation(_) => ControlFlow::Continue(()),
235 },
236 DFStatement::Explain(explain) => return collect(&explain.statement, calls),
237 _ => ControlFlow::Continue(()),
238 };
239 match flow {
240 ControlFlow::Continue(()) => Ok(()),
241 ControlFlow::Break(e) => Err(e),
242 }
243}
244
245fn default_normalize(ident: Ident) -> String {
247 match ident.quote_style {
248 Some(_) => ident.value,
249 None => ident.value.to_lowercase(),
250 }
251}
252
253fn function_name(name: &ObjectName, normalize: impl Fn(&Ident) -> String) -> Option<String> {
255 let [part] = name.0.as_slice() else {
256 return None;
257 };
258 part.as_ident().map(normalize)
259}
260
261pub fn paths_mut(factor: &mut TableFactor) -> Vec<&mut String> {
264 let TableFactor::Table {
265 name,
266 args: Some(args),
267 ..
268 } = factor
269 else {
270 return Vec::new();
271 };
272 let is_read = function_name(name, |ident| default_normalize(ident.clone()))
273 .is_some_and(|f| Function::lookup(&f).is_some());
274 let Some(FunctionArg::Unnamed(FunctionArgExpr::Expr(first))) = args.args.first_mut() else {
275 return Vec::new();
276 };
277 if !is_read {
278 return Vec::new();
279 }
280 fn string_mut(expr: &mut Expr) -> Option<&mut String> {
281 match expr {
282 Expr::Value(v) => match &mut v.value {
283 Value::SingleQuotedString(s) => Some(s),
284 _ => None,
285 },
286 Expr::Nested(inner) => string_mut(inner),
287 _ => None,
288 }
289 }
290 match first {
291 Expr::Array(array) => array.elem.iter_mut().filter_map(string_mut).collect(),
292 expr => string_mut(expr).into_iter().collect(),
293 }
294}
295
296fn call_key(name: &ObjectName, args: &TableFunctionArgs) -> String {
298 let args: Vec<String> = args.args.iter().map(ToString::to_string).collect();
299 format!("{name}({})", args.join(", "))
300}
301
302fn plan_error(message: String) -> DataFusionError {
303 DataFusionError::Plan(message)
304}
305
306fn parse_call(name: &str, function: Function, args: &TableFunctionArgs) -> Result<ReadCall> {
308 let usage = || match function {
309 Function::Parquet => format!("{name}('edges.parquet')"),
310 Function::Csv => format!("{name}('edges.csv', header => true)"),
311 Function::Json | Function::NdJson => format!("{name}('edges.jsonl')"),
312 };
313 if args.settings.is_some() {
314 return Err(plan_error(format!(
315 "{name}() takes no SETTINGS; write {}",
316 usage()
317 )));
318 }
319 let mut args = args.args.iter();
320 let locations = match args.next() {
321 Some(FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))) => locations(expr),
322 _ => None,
323 }
324 .ok_or_else(|| {
325 plan_error(format!(
326 "{name}() takes a path, or a list of paths, as its first argument; write {}",
327 usage()
328 ))
329 })?;
330 let mut options = FileOptions {
331 format: Some(function.format()),
332 ..FileOptions::default()
333 };
334 if function == Function::NdJson {
335 options.newline_delimited = Some(true);
336 }
337 let mut seen: Vec<String> = Vec::new();
338 for arg in args {
339 let (option, value) = match arg {
340 FunctionArg::Named {
341 name: option,
342 arg: FunctionArgExpr::Expr(value),
343 ..
344 } => (default_normalize(option.clone()), value),
345 FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::BinaryOp {
347 left,
348 op: BinaryOperator::Eq,
349 right,
350 })) if matches!(left.as_ref(), Expr::Identifier(_)) => {
351 let Expr::Identifier(option) = left.as_ref() else {
352 unreachable!("matched above")
353 };
354 (default_normalize(option.clone()), right.as_ref())
355 }
356 other => {
357 return Err(plan_error(format!(
358 "{name}() takes its options by name, as name => value; got {other}. \
359 Write {}",
360 usage()
361 )))
362 }
363 };
364 let known = function.options();
365 if !known.contains(&option.as_str()) {
366 let hint = closest_name(&option, known.iter().copied())
367 .map(|c| format!("; did you mean '{c}'?"))
368 .unwrap_or_default();
369 let takes = match known {
370 [] => "it takes none".to_string(),
371 known => format!("it takes {}", known.join(", ")),
372 };
373 return Err(plan_error(format!(
374 "{name}() has no option '{option}'{hint} ({takes})"
375 )));
376 }
377 let canonical = match option.as_str() {
378 "sep" | "delimiter" => "delim",
379 option => option,
380 };
381 if seen.iter().any(|s| s == canonical) {
382 return Err(plan_error(format!(
383 "{name}() was given '{option}' more than once"
384 )));
385 }
386 seen.push(canonical.to_string());
387 let invalid =
388 |what: &str| plan_error(format!("{name}({option} =>) must be {what}; got {value}"));
389 match canonical {
390 "header" => {
391 options.has_header = Some(boolean(value).ok_or_else(|| invalid("true or false"))?)
392 }
393 "delim" | "quote" => {
394 let byte = string(value)
395 .and_then(|s| single_byte(&s))
396 .ok_or_else(|| invalid("a string of one character, such as '|' or '\\t'"))?;
397 if canonical == "delim" {
398 options.delimiter = Some(byte);
399 } else {
400 options.quote = Some(byte);
401 }
402 }
403 "compression" => {
404 options.compression = compression(value)
405 .ok_or_else(|| invalid("'auto', 'none', 'gzip', 'bzip2', 'xz' or 'zstd'"))?
406 }
407 "format" => {
408 let format = string(value).map(|s| s.to_ascii_lowercase());
409 options.newline_delimited = match format.as_deref() {
410 Some("newline_delimited") => Some(true),
411 Some("array") => Some(false),
412 _ => return Err(invalid("'newline_delimited' or 'array'")),
413 }
414 }
415 _ => unreachable!("checked against the function's options"),
416 }
417 }
418 Ok(ReadCall { locations, options })
419}
420
421fn locations(expr: &Expr) -> Option<Vec<String>> {
423 fn path(expr: &Expr) -> Option<String> {
424 match expr {
425 Expr::Value(v) => match &v.value {
426 Value::SingleQuotedString(s) => Some(s.clone()),
427 _ => None,
428 },
429 Expr::Nested(inner) => path(inner),
430 _ => None,
431 }
432 }
433 match expr {
434 Expr::Array(array) => {
435 let paths: Option<Vec<String>> = array.elem.iter().map(path).collect();
436 paths.filter(|p| !p.is_empty())
437 }
438 expr => path(expr).map(|s| vec![s]),
439 }
440}
441
442fn string(expr: &Expr) -> Option<String> {
443 match expr {
444 Expr::Value(v) => match &v.value {
445 Value::SingleQuotedString(s) | Value::EscapedStringLiteral(s) => Some(s.clone()),
446 _ => None,
447 },
448 Expr::Nested(inner) => string(inner),
449 _ => None,
450 }
451}
452
453fn boolean(expr: &Expr) -> Option<bool> {
454 match expr {
455 Expr::Value(v) => match &v.value {
456 Value::Boolean(b) => Some(*b),
457 _ => None,
458 },
459 _ => None,
460 }
461}
462
463fn single_byte(s: &str) -> Option<u8> {
465 match s {
466 "\\t" => Some(b'\t'),
467 s if s.len() == 1 && s.is_ascii() => Some(s.as_bytes()[0]),
468 _ => None,
469 }
470}
471
472fn compression(expr: &Expr) -> Option<Option<CompressionTypeVariant>> {
474 let value = string(expr)?.to_ascii_lowercase();
475 match value.as_str() {
476 "auto" => Some(None),
477 "none" | "uncompressed" => Some(Some(CompressionTypeVariant::UNCOMPRESSED)),
478 other => other.parse().ok().map(Some),
479 }
480}
481
482#[derive(Debug)]
485pub struct ReadFunctions {
486 tables: Arc<ReadTables>,
487}
488
489impl ReadFunctions {
490 pub(crate) fn new(config: &SessionConfig) -> Result<Self> {
491 Ok(Self {
492 tables: read_tables(config)?,
493 })
494 }
495}
496
497impl RelationPlanner for ReadFunctions {
498 fn plan_relation(
499 &self,
500 relation: TableFactor,
501 context: &mut dyn RelationPlannerContext,
502 ) -> Result<RelationPlanning> {
503 let TableFactor::Table {
504 name,
505 args: Some(args),
506 ..
507 } = &relation
508 else {
509 return Ok(RelationPlanning::Original(Box::new(relation)));
510 };
511 let Some(function) = function_name(name, |ident| context.normalize_ident(ident.clone()))
512 else {
513 return Ok(RelationPlanning::Original(Box::new(relation)));
514 };
515 let Some(f) = Function::lookup(&function) else {
516 let given_a_path = matches!(
518 args.args.first(),
519 Some(FunctionArg::Unnamed(FunctionArgExpr::Expr(expr))) if locations(expr).is_some()
520 );
521 if given_a_path {
522 if let Some(close) = closest_name(&function, names()) {
523 return Err(plan_error(format!(
524 "there is no table function '{function}'; did you mean '{close}'?"
525 )));
526 }
527 }
528 return Ok(RelationPlanning::Original(Box::new(relation)));
529 };
530 let key = call_key(name, args);
531 let table = self
532 .tables
533 .tables
534 .lock()
535 .get(&key)
536 .map(|(t, _)| Arc::clone(t));
537 let Some(table) = table else {
538 parse_call(&function, f, args)?;
541 return Err(DataFusionError::Internal(format!(
542 "{function}() was not opened before planning; run the statement with \
543 ursa_sql::execute"
544 )));
545 };
546 let TableFactor::Table {
547 alias,
548 with_hints,
549 version,
550 with_ordinality,
551 partitions,
552 json_path,
553 sample,
554 index_hints,
555 ..
556 } = relation
557 else {
558 unreachable!("matched above")
559 };
560 if !with_hints.is_empty()
561 || version.is_some()
562 || with_ordinality
563 || !partitions.is_empty()
564 || json_path.is_some()
565 || sample.is_some()
566 || !index_hints.is_empty()
567 {
568 return Err(plan_error(format!("{function}() takes no table modifiers")));
569 }
570 let plan = LogicalPlanBuilder::scan(function.as_str(), provider_as_source(table), None)?
571 .build()?;
572 Ok(RelationPlanning::Planned(Box::new(PlannedRelation::new(
573 plan, alias,
574 ))))
575 }
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581 use datafusion::sql::sqlparser::dialect::GenericDialect;
582 use datafusion::sql::sqlparser::parser::Parser;
583
584 fn call(sql: &str) -> Result<ReadCall> {
585 let query = format!("SELECT * FROM {sql}");
586 let statement = Parser::parse_sql(&GenericDialect {}, &query)
587 .unwrap()
588 .remove(0);
589 let mut calls = Calls::default();
590 match statement.visit(&mut calls) {
591 ControlFlow::Break(e) => Err(e),
592 ControlFlow::Continue(()) => Ok(calls.calls.remove(0).1),
593 }
594 }
595
596 fn message(sql: &str) -> String {
597 crate::user_message(&call(sql).unwrap_err())
598 }
599
600 #[test]
601 fn reads_paths_and_options() {
602 let c = call("read_csv('a.csv', header => false, delim = '\\t', compression => 'gzip')")
603 .unwrap();
604 assert_eq!(c.locations, ["a.csv"]);
605 assert_eq!(c.options.format, Some(FileFormat::Csv));
606 assert_eq!(c.options.has_header, Some(false));
607 assert_eq!(c.options.delimiter, Some(b'\t'));
608 assert_eq!(c.options.compression, Some(CompressionTypeVariant::GZIP));
609
610 let c = call("read_parquet(['a.parquet', 'b.parquet'])").unwrap();
611 assert_eq!(c.locations, ["a.parquet", "b.parquet"]);
612
613 let c = call("read_json('a.json', format => 'array')").unwrap();
614 assert_eq!(c.options.newline_delimited, Some(false));
615 let c = call("read_ndjson('a.txt', compression => 'auto')").unwrap();
616 assert_eq!(c.options.newline_delimited, Some(true));
617 assert_eq!(c.options.compression, None);
618 }
619
620 #[test]
621 fn reports_bad_arguments() {
622 assert_eq!(
623 message("read_csv('a.csv', hedaer => true)"),
624 "read_csv() has no option 'hedaer'; did you mean 'header'? (it takes header, \
625 delim, sep, delimiter, quote, compression)"
626 );
627 assert_eq!(
628 message("read_parquet('a.parquet', header => true)"),
629 "read_parquet() has no option 'header' (it takes none)"
630 );
631 assert_eq!(
632 message("read_csv(1)"),
633 "read_csv() takes a path, or a list of paths, as its first argument; write \
634 read_csv('edges.csv', header => true)"
635 );
636 assert_eq!(
637 message("read_csv('a.csv', delim => ';;')"),
638 "read_csv(delim =>) must be a string of one character, such as '|' or '\\t'; \
639 got ';;'"
640 );
641 assert_eq!(
642 message("read_csv('a.csv', sep => ';', delim => ',')"),
643 "read_csv() was given 'delim' more than once"
644 );
645 }
646}