use crate::ast::*;
use crate::error::{BqlError, BqlResult};
pub fn format(query: Option<&Expr>) -> String {
match query {
None => String::new(),
Some(q) => format_expr(q),
}
}
fn format_and(v: &And<'_>) -> String {
v.operands
.iter()
.map(|x| format_expr_child(x, 1))
.collect::<Vec<_>>()
.join(" AND ")
}
fn format_bin_op(v: &BinOp) -> String {
match v {
BinOp::Eq => "=".to_string(),
BinOp::Neq => "!=".to_string(),
BinOp::Contains => "~".to_string(),
BinOp::NotContains => "!~".to_string(),
BinOp::StartsWith => "^=".to_string(),
BinOp::NotStartsWith => "!^=".to_string(),
BinOp::EndsWith => "$=".to_string(),
BinOp::NotEndsWith => "!$=".to_string(),
BinOp::Gt => ">".to_string(),
BinOp::Gte => ">=".to_string(),
BinOp::Lt => "<".to_string(),
BinOp::Lte => "<=".to_string(),
}
}
fn format_boolean(v: &Boolean) -> String {
match v {
Boolean::True => "true".to_string(),
Boolean::False => "false".to_string(),
}
}
fn expr_precedence(v: &Expr<'_>) -> usize {
match v {
Expr::And(_) => 1,
Expr::Or(_) => 0,
Expr::Not(_) => 2,
Expr::Compare(_) => 3,
Expr::Between(_) => 3,
Expr::NotBetween(_) => 3,
Expr::In(_) => 3,
Expr::NotIn(_) => 3,
Expr::IsEmpty(_) => 3,
Expr::IsNull(_) => 3,
Expr::IsNotEmpty(_) => 3,
Expr::IsNotNull(_) => 3,
}
}
fn format_expr(v: &Expr<'_>) -> String {
match v {
Expr::And(x) => format_and(x),
Expr::Or(x) => format_or(x),
Expr::Not(x) => format!("NOT {}", format_expr_child(x, 2)),
Expr::Compare(x) => format_compare(x),
Expr::Between(x) => format_between(x),
Expr::NotBetween(x) => format_not_between(x),
Expr::In(x) => format_in(x),
Expr::NotIn(x) => format_not_in(x),
Expr::IsEmpty(x) => format_is_empty(x),
Expr::IsNull(x) => format_is_null(x),
Expr::IsNotEmpty(x) => format_is_not_empty(x),
Expr::IsNotNull(x) => format_is_not_null(x),
}
}
fn format_expr_child(v: &Expr<'_>, parent: usize) -> String {
let s = format_expr(v);
if expr_precedence(v) < parent {
format!("({s})")
} else {
s
}
}
fn format_compare(v: &Compare<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
format_bin_op(&v.op),
format_value(&v.value),
])
}
fn format_between(v: &Between<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"BETWEEN".to_string(),
format_value(&v.lo),
"AND".to_string(),
format_value(&v.hi),
])
}
fn format_not_between(v: &Between<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"NOT".to_string(),
"BETWEEN".to_string(),
format_value(&v.lo),
"AND".to_string(),
format_value(&v.hi),
])
}
fn format_in(v: &In<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"IN".to_string(),
"(".to_string(),
v.values
.iter()
.map(|x| format_value(x))
.collect::<Vec<_>>()
.join(", "),
")".to_string(),
])
}
fn format_not_in(v: &In<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"NOT".to_string(),
"IN".to_string(),
"(".to_string(),
v.values
.iter()
.map(|x| format_value(x))
.collect::<Vec<_>>()
.join(", "),
")".to_string(),
])
}
fn format_is_empty(v: &Empty<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"IS".to_string(),
"EMPTY".to_string(),
])
}
fn format_is_null(v: &Empty<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"IS".to_string(),
"NULL".to_string(),
])
}
fn format_is_not_empty(v: &Empty<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"IS".to_string(),
"NOT".to_string(),
"EMPTY".to_string(),
])
}
fn format_is_not_null(v: &Empty<'_>) -> String {
join_pieces(&[
if is_safe_bare_word(&v.field) {
v.field.to_string()
} else {
quote_string(&v.field)
},
"IS".to_string(),
"NOT".to_string(),
"NULL".to_string(),
])
}
fn format_now(_v: &Now) -> String {
"now()".to_string()
}
fn format_or(v: &Or<'_>) -> String {
v.operands
.iter()
.map(|x| format_expr_child(x, 0))
.collect::<Vec<_>>()
.join(" OR ")
}
fn format_participant(v: &Participant<'_>) -> String {
format!("{}:{}", v.r#type, v.id)
}
fn format_value(v: &Value<'_>) -> String {
match v {
Value::String(s) => quote_string(s),
Value::Enum(s) => {
if is_safe_bare_word(s)
&& matches!(crate::parser::classify_word(s, 0), Ok(Value::Enum(_)))
{
s.to_string()
} else {
quote_string(&escape(s))
}
}
Value::Integer(s) => s.to_string(),
Value::Float(s) => s.to_string(),
Value::Boolean(x) => format_boolean(x),
Value::Date(s) => s.to_string(),
Value::DateTime(s) => s.to_string(),
Value::Participant(x) => format_participant(x),
Value::Now(x) => format_now(x),
}
}
fn quote_string(s: &str) -> String {
format!("\"{s}\"")
}
pub fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c == '"' || c == '\\' {
out.push('\\');
}
out.push(c);
}
out
}
pub fn unescape(s: &str) -> BqlResult<String> {
let mut out = String::with_capacity(s.len());
let mut chars = s.char_indices();
while let Some((i, c)) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
let Some((_, e)) = chars.next() else {
return Err(BqlError::new(i, "unterminated escape in string literal"));
};
match e {
'"' => out.push('"'),
'\\' => out.push('\\'),
'/' => out.push('/'),
'n' => out.push('\n'),
't' => out.push('\t'),
'r' => out.push('\r'),
'b' => out.push('\u{8}'),
'f' => out.push('\u{c}'),
'u' => {
let mut hex = String::new();
for _ in 0..4 {
match chars.next() {
Some((_, h)) => hex.push(h),
None => {
return Err(BqlError::new(
i,
"incomplete `\\u` escape in string literal",
));
}
}
}
let cp = u32::from_str_radix(&hex, 16)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| BqlError::new(i, "invalid `\\u` escape in string literal"))?;
out.push(cp);
}
other => {
return Err(BqlError::new(
i,
format!("invalid escape sequence `\\{other}` in string literal"),
));
}
}
}
Ok(out)
}
fn is_safe_bare_word(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' | ':' | '.' | '+'))
&& !is_keyword(s)
}
fn is_keyword(s: &str) -> bool {
matches!(
s.to_ascii_lowercase().as_str(),
"and" | "between" | "empty" | "false" | "in" | "is" | "not" | "null" | "or" | "true"
)
}
fn join_pieces(parts: &[String]) -> String {
let mut out = String::new();
for p in parts.iter().filter(|p| !p.is_empty()) {
if let (Some(prev), Some(cur)) = (out.chars().last(), p.chars().next()) {
let no_space = matches!(cur, ')' | ',' | ':') || matches!(prev, '(' | ':');
if !no_space {
out.push(' ');
}
}
out.push_str(p);
}
out
}