use std::ops::ControlFlow;
use datafusion::common::TableReference;
use datafusion::prelude::SessionContext;
use datafusion::sql::parser::Statement as DFStatement;
use datafusion::sql::sqlparser::ast::{
visit_relations_mut, Ident, ObjectName, ObjectNamePart, Statement as SQLStatement,
};
use datafusion::sql::sqlparser::dialect::dialect_from_str;
use datafusion::sql::sqlparser::tokenizer::{Token, Tokenizer};
use ursa_engine::paths::looks_like_location;
use crate::parse_sql_for;
use crate::parser::{byte_offset, UrsaStatement};
pub(crate) type Replacement = (String, String);
pub(crate) fn rewrite_locations(
ctx: &SessionContext,
statement: &mut UrsaStatement,
mut map: impl FnMut(&str) -> Option<String>,
) -> Vec<Replacement> {
let mut changed = Vec::new();
let mut rewrite = |location: &mut String| {
if let Some(new) = map(location) {
changed.push((std::mem::replace(location, new.clone()), new));
}
};
match statement {
UrsaStatement::DataFusion(statement) => match statement.as_mut() {
DFStatement::CreateExternalTable(table) => {
for location in &mut table.locations {
rewrite(location);
}
}
DFStatement::Statement(s) => {
if let SQLStatement::CreateView(view) = s.as_mut() {
let _ = visit_relations_mut(view.query.as_mut(), |name| {
if let Some(ident) = file_name(ctx, name) {
rewrite(&mut ident.value);
}
ControlFlow::<()>::Continue(())
});
}
}
_ => {}
},
UrsaStatement::CreatePropertyGraph(graph) => {
let mut names = vec![&mut graph.edges.table];
if let Some(vertices) = &mut graph.vertices {
names.push(&mut vertices.table);
}
for endpoint in [&mut graph.edges.source, &mut graph.edges.destination] {
if let Some(references) = &mut endpoint.references {
names.push(&mut references.table);
}
}
for name in names {
if let Some(ident) = file_name(ctx, name) {
rewrite(&mut ident.value);
}
}
}
_ => {}
}
changed
}
fn file_name<'a>(ctx: &SessionContext, name: &'a mut ObjectName) -> Option<&'a mut Ident> {
let [ObjectNamePart::Identifier(ident)] = name.0.as_mut_slice() else {
return None;
};
let is_file = ident.quote_style.is_some()
&& looks_like_location(&ident.value)
&& !ctx
.table_exist(TableReference::bare(ident.value.as_str()))
.unwrap_or(false);
is_file.then_some(ident)
}
pub(crate) fn rewrite_text(
ctx: &SessionContext,
text: &str,
replacements: &[Replacement],
expected: &UrsaStatement,
) -> Option<String> {
let dialect_name = ctx.state().config().options().sql_parser.dialect;
let dialect = dialect_from_str(dialect_name)?;
let tokens = Tokenizer::new(dialect.as_ref(), text)
.tokenize_with_location()
.ok()?;
let replacement = |value: &str| {
replacements
.iter()
.find(|(old, _)| old == value)
.map(|(_, new)| new)
};
let mut out = String::with_capacity(text.len());
let mut copied = 0;
for token in &tokens {
let (value, quote) = match &token.token {
Token::SingleQuotedString(s) => (s, '\''),
Token::Word(w) => match w.quote_style {
Some(q @ ('"' | '\'' | '`')) => (&w.value, q),
_ => continue,
},
_ => continue,
};
let Some(new) = replacement(value) else {
continue;
};
let start = byte_offset(text, token.span.start)?;
let end = byte_offset(text, token.span.end)?;
if start < copied || end <= start {
return None;
}
out.push_str(&text[copied..start]);
out.push_str(&Ident::with_quote(quote, new.as_str()).to_string());
copied = end;
}
out.push_str(&text[copied..]);
let parsed = parse_sql_for(ctx, &out).ok()?;
(parsed.len() == 1 && parsed[0] == *expected).then_some(out)
}