use super::{SQLError, Value};
pub(super) fn render_value(value: &Value) -> Result<String, SQLError> {
let rendered = match value {
Value::Null => "NULL".into(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(f) if f.is_finite() => format!("{f}"),
Value::Float(f) => {
return Err(SQLError::TypeMismatch(format!(
"non-finite filter value `{f}` cannot be rendered as SQL"
)));
}
Value::Decimal(d) => d.to_sql_string(),
Value::Str(s) | Value::FixedChar(s) => quote_str(s),
Value::Json(text) => format!("{}::json", quote_str(text)),
Value::JsonB(text) => format!("{}::jsonb", quote_str(text)),
Value::Bytes(bytes) => format!("decode('{}', 'hex')", hex_encode(bytes)?),
Value::Temporal(t) => quote_str(&t.to_sql_string()),
Value::Array(array) => quote_str(&uqa_sql::expr::array_value_to_string(array)),
Value::List(items) => {
let inner: Vec<String> = items.iter().map(render_value).collect::<Result<_, _>>()?;
format!("ARRAY[{}]", inner.join(", "))
}
Value::Row(items) => {
let inner: Vec<String> = items.iter().map(render_value).collect::<Result<_, _>>()?;
format!("ROW({})", inner.join(", "))
}
Value::Record(fields) => {
let inner: Vec<String> = fields
.iter()
.map(|(_, value)| render_value(value))
.collect::<Result<_, _>>()?;
format!("ROW({})", inner.join(", "))
}
Value::Map(_) => {
return Err(SQLError::TypeMismatch(
"map filter values do not have an unambiguous SQL literal".into(),
));
}
};
Ok(rendered)
}
fn hex_encode(bytes: &[u8]) -> Result<String, SQLError> {
const HEX: &[u8; 16] = b"0123456789abcdef";
let capacity = bytes
.len()
.checked_mul(2)
.ok_or_else(|| SQLError::TypeMismatch("byte literal length overflow".into()))?;
let mut encoded = String::new();
encoded
.try_reserve_exact(capacity)
.map_err(|error| SQLError::TypeMismatch(format!("allocate byte literal: {error}")))?;
for byte in bytes {
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
Ok(encoded)
}
pub(super) fn quote_str(s: &str) -> String {
let escaped = s.replace('\'', "''");
format!("'{escaped}'")
}