use std::fmt::Write as _;
use uqa_core::Value;
use crate::{FDWPredicate, ForeignTable, PredicateOp};
pub fn quote_literal(value: &Value) -> Result<String, ArrowFlightPrepareError> {
Ok(match value {
Value::Null => "NULL".into(),
Value::Bool(b) => {
if *b {
"TRUE".into()
} else {
"FALSE".into()
}
}
Value::Int(i) => i.to_string(),
Value::Float(f) if f.is_finite() => format!("{f}"),
Value::Float(f) => {
return Err(ArrowFlightPrepareError::UnsupportedLiteral(format!(
"non-finite float {f}"
)));
}
Value::Decimal(d) => d.to_sql_string(),
Value::Str(s) | Value::FixedChar(s) => {
let escaped = s.replace('\'', "''");
format!("'{escaped}'")
}
Value::Json(text) | Value::JsonB(text) => {
let escaped = text.replace('\'', "''");
let type_name = if matches!(value, Value::Json(_)) {
"JSON"
} else {
"JSONB"
};
format!("CAST('{escaped}' AS {type_name})")
}
Value::Bytes(b) => {
let capacity = b.len().checked_mul(2).ok_or_else(|| {
ArrowFlightPrepareError::UnsupportedLiteral(
"binary literal length overflows usize".into(),
)
})?;
let mut hex = String::new();
hex.try_reserve_exact(capacity).map_err(|error| {
ArrowFlightPrepareError::UnsupportedLiteral(format!(
"failed to allocate binary literal: {error}"
))
})?;
for byte in b {
write!(hex, "{byte:02X}").map_err(|error| {
ArrowFlightPrepareError::UnsupportedLiteral(format!(
"failed to encode binary literal: {error}"
))
})?;
}
format!("X'{hex}'")
}
Value::Temporal(t) => {
let escaped = t.to_sql_string().replace('\'', "''");
format!("'{escaped}'")
}
Value::Array(array) => {
if array.lower_bounds().iter().any(|lower| *lower != 1) {
return Err(ArrowFlightPrepareError::UnsupportedLiteral(
"Flight SQL cannot portably represent non-one-based array literals".into(),
));
}
format!(
"ARRAY[{}]",
array
.elements()
.iter()
.map(quote_literal)
.collect::<Result<Vec<_>, _>>()?
.join(", ")
)
}
Value::List(items) => {
items
.iter()
.map(quote_literal)
.collect::<Result<Vec<_>, _>>()?
.join(", ")
}
Value::Row(_) | Value::Record(_) | Value::Map(_) => {
return Err(ArrowFlightPrepareError::UnsupportedLiteral(
"composite and map values have no portable Flight SQL literal".into(),
));
}
})
}
pub fn build_where_clause(predicates: &[FDWPredicate]) -> Result<String, ArrowFlightPrepareError> {
let mut clauses: Vec<String> = Vec::with_capacity(predicates.len());
for p in predicates {
let column = quote_identifier(&p.column);
match (&p.value, p.operator) {
(Value::Null, PredicateOp::Eq) => {
clauses.push(format!("{column} IS NULL"));
}
(Value::Null, PredicateOp::NotEq) => {
clauses.push(format!("{column} IS NOT NULL"));
}
(Value::Null, operator) => {
return Err(ArrowFlightPrepareError::InvalidPredicate(format!(
"{operator:?} cannot compare `{}` with NULL",
p.column
)));
}
(Value::List(items), PredicateOp::In) => {
if items.is_empty() {
clauses.push("FALSE".to_string());
continue;
}
let inner = items
.iter()
.map(quote_literal)
.collect::<Result<Vec<_>, _>>()?
.join(", ");
clauses.push(format!("{column} IN ({inner})"));
}
(_, PredicateOp::In) => {
return Err(ArrowFlightPrepareError::InvalidPredicate(format!(
"IN on `{}` requires a list",
p.column
)));
}
(_, op) => {
let escape = matches!(
op,
PredicateOp::Like
| PredicateOp::NotLike
| PredicateOp::ILike
| PredicateOp::NotILike
)
.then_some(" ESCAPE '\\'")
.unwrap_or_default();
clauses.push(format!(
"{column} {} {}{escape}",
op.sql_token(),
quote_literal(&p.value)?
));
}
}
}
Ok(clauses.join(" AND "))
}
pub fn prepare_query(
table: &ForeignTable,
columns: Option<&[String]>,
predicates: &[FDWPredicate],
limit: Option<u64>,
) -> Result<String, ArrowFlightPrepareError> {
let (source, default_to_star) = if let Some(query) = table.options.get("query") {
let query = query.trim().trim_end_matches(';').trim();
if query.is_empty() {
return Err(ArrowFlightPrepareError::EmptyQuery(table.name.clone()));
}
(format!("({query}) AS uqa_fdw_source"), true)
} else {
let Some(source) = table.options.get("source") else {
return Err(ArrowFlightPrepareError::MissingSourceOrQuery(
table.name.clone(),
));
};
(quote_identifier(source), false)
};
let column_names = match columns {
Some(columns) if !columns.is_empty() => columns.to_vec(),
_ if default_to_star => Vec::new(),
_ => table
.columns
.iter()
.map(|column| column.name.clone())
.collect(),
};
let columns = if column_names.is_empty() {
"*".to_string()
} else {
column_names
.iter()
.map(|column| quote_identifier(column))
.collect::<Vec<_>>()
.join(", ")
};
let mut query = format!("SELECT {columns} FROM {source}");
if !predicates.is_empty() {
let where_sql = build_where_clause(predicates)?;
if !where_sql.is_empty() {
query.push_str(" WHERE ");
query.push_str(&where_sql);
}
}
if let Some(n) = limit {
use std::fmt::Write as _;
write!(query, " LIMIT {n}").map_err(|error| {
ArrowFlightPrepareError::UnsupportedLiteral(format!("failed to append LIMIT: {error}"))
})?;
}
Ok(query)
}
fn quote_identifier(value: &str) -> String {
value
.split('.')
.map(|part| {
if part == "*" {
"*".to_string()
} else {
format!("\"{}\"", part.replace('"', "\"\""))
}
})
.collect::<Vec<_>>()
.join(".")
}
#[derive(Debug, thiserror::Error)]
pub enum ArrowFlightPrepareError {
#[error("Foreign table `{0}` missing required option `source` or `query`")]
MissingSourceOrQuery(String),
#[error("Foreign table `{0}` has an empty `query` option")]
EmptyQuery(String),
#[error("Invalid Arrow Flight pushdown predicate: {0}")]
InvalidPredicate(String),
#[error("Unsupported Arrow Flight SQL literal: {0}")]
UnsupportedLiteral(String),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ColumnDef, ColumnType};
use std::collections::BTreeMap;
fn table_with_options<const N: usize>(opts: [(&str, &str); N]) -> ForeignTable {
ForeignTable {
name: "remote".into(),
server_name: "flight".into(),
columns: vec![
ColumnDef {
name: "id".into(),
ty: ColumnType::Integer,
},
ColumnDef {
name: "name".into(),
ty: ColumnType::Text,
},
],
options: opts
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect::<BTreeMap<_, _>>(),
}
}
#[test]
fn quote_literal_escapes_single_quotes() {
assert_eq!(
quote_literal(&Value::Str("it's".into())).unwrap(),
"'it''s'"
);
}
#[test]
fn quote_literal_renders_int_and_bool() {
assert_eq!(quote_literal(&Value::Int(42)).unwrap(), "42");
assert_eq!(quote_literal(&Value::Bool(true)).unwrap(), "TRUE");
}
#[test]
fn build_where_clause_inlines_literals() {
let preds = vec![
FDWPredicate {
column: "year".into(),
operator: PredicateOp::Eq,
value: Value::Int(2024),
},
FDWPredicate {
column: "name".into(),
operator: PredicateOp::Like,
value: Value::Str("alpha%".into()),
},
];
let sql = build_where_clause(&preds).unwrap();
assert_eq!(
sql,
"\"year\" = 2024 AND \"name\" LIKE 'alpha%' ESCAPE '\\'"
);
}
#[test]
fn build_where_clause_emits_in_list() {
let preds = vec![FDWPredicate {
column: "country".into(),
operator: PredicateOp::In,
value: Value::List(vec![Value::Str("US".into()), Value::Str("KR".into())]),
}];
let sql = build_where_clause(&preds).unwrap();
assert_eq!(sql, "\"country\" IN ('US', 'KR')");
}
#[test]
fn prepare_query_assembles_select_when_no_query_option() {
let table = table_with_options([("source", "books")]);
let q = prepare_query(&table, None, &[], None).unwrap();
assert_eq!(q, "SELECT \"id\", \"name\" FROM \"books\"");
}
#[test]
fn prepare_query_wraps_query_option_and_preserves_pushdown() {
let table = table_with_options([("query", "SELECT id FROM books WHERE year = 2024")]);
let preds = vec![FDWPredicate {
column: "year".into(),
operator: PredicateOp::Eq,
value: Value::Int(2025),
}];
let q = prepare_query(&table, None, &preds, None).unwrap();
assert_eq!(
q,
"SELECT * FROM (SELECT id FROM books WHERE year = 2024) AS uqa_fdw_source WHERE \"year\" = 2025"
);
}
#[test]
fn prepare_query_appends_limit_when_absent() {
let table = table_with_options([("source", "books")]);
let q = prepare_query(&table, None, &[], Some(50)).unwrap();
assert!(q.ends_with(" LIMIT 50"));
}
#[test]
fn prepare_query_errors_when_source_and_query_missing() {
let table = table_with_options([]);
let err = prepare_query(&table, None, &[], None).unwrap_err();
assert!(matches!(
err,
ArrowFlightPrepareError::MissingSourceOrQuery(name) if name == "remote"
));
}
#[test]
fn unsupported_literals_and_malformed_in_predicates_fail() {
assert!(quote_literal(&Value::Map(BTreeMap::new())).is_err());
assert!(quote_literal(&Value::Float(f64::NAN)).is_err());
let predicate = FDWPredicate {
column: "id".into(),
operator: PredicateOp::In,
value: Value::Int(1),
};
assert!(build_where_clause(&[predicate]).is_err());
}
}