use super::*;
pub(crate) fn column_collation(e: &Expr, ctx: &EvalContext<'_>) -> Option<spg_storage::Collation> {
let Expr::Column(c) = e else {
return None;
};
let matches_composite = |s: &str| {
c.qualifier.as_deref().is_some_and(|q| {
s.len() == q.len() + 1 + c.name.len()
&& s.as_bytes()[q.len()] == b'.'
&& s.starts_with(q)
&& s.ends_with(c.name.as_str())
})
};
if c.qualifier.is_some()
&& let Some(s) = ctx.columns.iter().find(|s| matches_composite(&s.name))
{
return Some(s.collation);
}
if let Some(s) = ctx.columns.iter().find(|s| s.name == c.name) {
return Some(s.collation);
}
let ends_with_dot_name = |s: &str| {
s.len() > c.name.len()
&& s.ends_with(c.name.as_str())
&& s.as_bytes()[s.len() - c.name.len() - 1] == b'.'
};
let mut matches = ctx.columns.iter().filter(|s| ends_with_dot_name(&s.name));
let first = matches.next();
let extra = matches.next();
match (first, extra) {
(Some(s), None) => Some(s.collation),
_ => None,
}
}
pub(super) fn collation_fold_for_compare(
op: BinOp,
lhs: &Expr,
rhs: &Expr,
l: Value<'static>,
r: Value<'static>,
ctx: &EvalContext<'_>,
) -> (Value<'static>, Value<'static>) {
if ctx.mysql_dialect && super::is_mysql_numeric_binop(op) {
let fold_set = |expr: &Expr, v: Value<'static>| -> Value<'static> {
match &v {
Value::Text(s) => {
if let Some(variants) = super::expr_set_variants(expr, ctx.columns) {
Value::BigInt(super::set_text_to_bitmask(s, variants))
} else if let Some(variants) =
super::expr_inline_enum_variants(expr, ctx.columns)
{
Value::BigInt(super::enum_text_to_ordinal(s, variants))
} else {
v
}
}
_ => v,
}
};
return (fold_set(lhs, l), fold_set(rhs, r));
}
if !matches!(
op,
BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
) {
return (l, r);
}
let any_binary = is_binary_coerced(lhs)
|| is_binary_coerced(rhs)
|| operand_is_binary_column(lhs, ctx)
|| operand_is_binary_column(rhs, ctx);
let mysql = ctx.mysql_dialect && !any_binary;
let lhs_col = column_collation(lhs, ctx);
let rhs_col = column_collation(rhs, ctx);
let ci = matches!(lhs_col, Some(spg_storage::Collation::CaseInsensitive))
|| matches!(rhs_col, Some(spg_storage::Collation::CaseInsensitive));
if !ci && !mysql {
return (l, r);
}
let fold = |v: Value<'static>| match v {
Value::Text(s) if mysql => Value::text(spg_storage::mysql_compare_fold(&s)),
Value::Text(s) => Value::text(s.to_ascii_lowercase()),
other => other,
};
(fold(l), fold(r))
}
pub(super) fn eval_expr_cow<'r>(
expr: &Expr,
row: &'r Row<'static>,
ctx: &EvalContext<'_>,
) -> Result<Cow<'r, Value<'static>>, EvalError> {
match expr {
Expr::Column(c) => match resolve_column_borrowed(c, row, ctx)? {
Some(v) => Ok(Cow::Borrowed(v)),
None => resolve_column(c, row, ctx).map(Cow::Owned),
},
_ => eval_expr(expr, row, ctx).map(Cow::Owned),
}
}
#[inline]
pub(super) fn is_owned_compare_value(v: &Value) -> bool {
matches!(v, Value::Numeric { .. } | Value::Interval { .. })
}
#[inline]
pub(super) fn compare_is_case_insensitive(lhs: &Expr, rhs: &Expr, ctx: &EvalContext<'_>) -> bool {
if is_binary_coerced(lhs) || is_binary_coerced(rhs) {
return false;
}
if ctx.mysql_dialect {
return !operand_is_binary_column(lhs, ctx) && !operand_is_binary_column(rhs, ctx);
}
matches!(
column_collation(lhs, ctx),
Some(spg_storage::Collation::CaseInsensitive)
) || matches!(
column_collation(rhs, ctx),
Some(spg_storage::Collation::CaseInsensitive)
)
}
pub(super) fn mysql_text_fold_applies(lhs: &Expr, rhs: &Expr, ctx: &EvalContext<'_>) -> bool {
ctx.mysql_dialect
&& !is_binary_coerced(lhs)
&& !is_binary_coerced(rhs)
&& !operand_is_binary_column(lhs, ctx)
&& !operand_is_binary_column(rhs, ctx)
}
pub(super) fn operand_is_binary_column(e: &Expr, ctx: &EvalContext<'_>) -> bool {
matches!(
column_collation(e, ctx),
Some(spg_storage::Collation::Binary)
)
}
pub(crate) fn is_binary_coerced(e: &Expr) -> bool {
matches!(
e,
Expr::Cast {
target: spg_sql::ast::CastTarget::Named(n),
..
} if n.eq_ignore_ascii_case("binary") || n.to_ascii_lowercase().starts_with("binary(")
)
}
#[inline]
pub(super) fn composite_eq(schema_name: &str, qualifier: &str, name: &str) -> bool {
schema_name.len() == qualifier.len() + 1 + name.len()
&& schema_name.as_bytes()[qualifier.len()] == b'.'
&& schema_name[..qualifier.len()] == *qualifier
&& schema_name[qualifier.len() + 1..] == *name
}
pub(crate) fn find_column_pos(c: &ColumnName, ctx: &EvalContext<'_>) -> Option<usize> {
if let Some(q) = &c.qualifier {
if let Some(pos) = ctx
.columns
.iter()
.position(|s| composite_eq(&s.name, q, &c.name))
{
return Some(pos);
}
}
if let Some(pos) = ctx.columns.iter().position(|s| s.name == c.name) {
return Some(pos);
}
let suffix_at = |s: &str| s.len().checked_sub(c.name.len() + 1);
let mut matches = ctx.columns.iter().enumerate().filter(|(_, s)| {
suffix_at(&s.name)
.is_some_and(|dot| s.name.as_bytes()[dot] == b'.' && s.name[dot + 1..] == *c.name)
});
match (matches.next(), matches.next()) {
(Some((pos, _)), None) => Some(pos),
_ => None,
}
}
pub(super) fn resolve_column_borrowed<'r, 'a>(
c: &ColumnName,
row: &'r Row<'a>,
ctx: &EvalContext<'_>,
) -> Result<Option<&'r Value<'a>>, EvalError> {
let is_composite = |pos: usize| {
ctx.columns
.get(pos)
.is_some_and(|s| s.user_composite_type.is_some())
};
if let Some(q) = &c.qualifier {
if let Some(pos) = ctx
.columns
.iter()
.position(|s| composite_eq(&s.name, q, &c.name))
{
if is_composite(pos) {
return Ok(None);
}
return Ok(row.values.get(pos));
}
}
if let Some(pos) = ctx.columns.iter().position(|s| s.name == c.name) {
if is_composite(pos) {
return Ok(None);
}
return Ok(row.values.get(pos));
}
Ok(None)
}
pub(super) fn text_prefix_chars(t: &str, n: i64) -> String {
if n >= 0 {
let n = usize::try_from(n).unwrap_or(usize::MAX);
match t.char_indices().nth(n) {
Some((byte_idx, _)) => t[..byte_idx].into(),
None => t.into(),
}
} else {
let drop_tail = usize::try_from(-n).unwrap_or(usize::MAX);
let total = t.chars().count();
let keep = total.saturating_sub(drop_tail);
match t.char_indices().nth(keep) {
Some((byte_idx, _)) => t[..byte_idx].into(),
None => t.into(),
}
}
}
pub(crate) fn locate_column(
c: &ColumnName,
ctx: &EvalContext<'_>,
) -> Result<Option<usize>, EvalError> {
if let Some(q) = &c.qualifier {
if let Some(pos) = ctx
.columns
.iter()
.position(|s| composite_eq(&s.name, q, &c.name))
{
return Ok(Some(pos));
}
let prefix = alloc::format!("{q}.");
if ctx.columns.iter().any(|sc| sc.name.starts_with(&prefix)) {
return Err(EvalError::ColumnNotFound {
name: alloc::format!("{q}.{name}", name = c.name),
});
}
let expected = ctx.table_alias.ok_or_else(|| EvalError::UnknownQualifier {
qualifier: q.clone(),
})?;
if q != expected {
return Err(EvalError::UnknownQualifier {
qualifier: q.clone(),
});
}
}
if let Some(pos) = ctx.columns.iter().position(|s| s.name == c.name) {
return Ok(Some(pos));
}
let suffix = alloc::format!(".{name}", name = c.name);
let mut matches = ctx
.columns
.iter()
.enumerate()
.filter(|(_, s)| s.name.ends_with(&suffix));
let first = matches.next();
let extra = matches.next();
match (first, extra) {
(Some((pos, _)), None) => Ok(Some(pos)),
(Some(_), Some(_)) => Err(EvalError::TypeMismatch {
detail: alloc::format!("column reference \"{}\" is ambiguous", c.name),
}),
_ => {
if c.qualifier.is_none()
&& (ctx.table_alias == Some(c.name.as_str()) || {
let prefix = alloc::format!("{name}.", name = c.name);
ctx.columns.iter().any(|s| s.name.starts_with(&prefix))
})
{
return Ok(None);
}
Err(EvalError::ColumnNotFound {
name: c.name.clone(),
})
}
}
}
pub(crate) fn column_at(
pos: usize,
row: &Row<'_>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
rehydrate_cell(pos, row, ctx)
}
pub(super) fn resolve_column(
c: &ColumnName,
row: &Row<'_>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
match locate_column(c, ctx)? {
Some(pos) => rehydrate_cell(pos, row, ctx),
None => whole_row_composite(row, ctx, &c.name),
}
}
fn whole_row_composite(
row: &Row<'_>,
ctx: &EvalContext<'_>,
alias: &str,
) -> Result<Value<'static>, EvalError> {
let prefix = alloc::format!("{alias}.");
let joined: Vec<(usize, &str)> = ctx
.columns
.iter()
.enumerate()
.filter_map(|(i, s)| s.name.strip_prefix(&prefix).map(|bare| (i, bare)))
.collect();
if ctx.columns.len() == 1 && ctx.columns[0].scalar_row_source {
return rehydrate_cell(0, row, ctx);
}
let fields: Vec<(String, Value<'static>)> = if joined.is_empty() {
ctx.columns
.iter()
.enumerate()
.map(|(i, s)| Ok((s.name.clone(), rehydrate_cell(i, row, ctx)?)))
.collect::<Result<_, EvalError>>()?
} else {
joined
.into_iter()
.map(|(i, bare)| Ok((bare.to_string(), rehydrate_cell(i, row, ctx)?)))
.collect::<Result<_, EvalError>>()?
};
Ok(Value::Composite(fields))
}
fn rehydrate_cell(
pos: usize,
row: &Row<'_>,
ctx: &EvalContext<'_>,
) -> Result<Value<'static>, EvalError> {
let v = row.values[pos].clone().into_owned();
let Some(cname) = ctx
.columns
.get(pos)
.and_then(|c| c.user_composite_type.as_deref())
else {
return Ok(v);
};
Ok(json_to_composite(&v, cname, ctx).unwrap_or(v))
}
pub(crate) fn json_to_composite(
v: &Value<'_>,
type_name: &str,
ctx: &EvalContext<'_>,
) -> Option<Value<'static>> {
let (Value::Json(src) | Value::Text(src)) = v else {
return None;
};
let def = ctx.catalog?.composite_types().get(type_name)?;
let parsed = crate::json::parse(src.as_ref()).ok()?;
let crate::json::JsonValue::Object(entries) = parsed else {
return None;
};
let mut fields: alloc::vec::Vec<(alloc::string::String, Value<'static>)> =
alloc::vec::Vec::with_capacity(def.fields.len());
for (i, (fname, fty)) in def.fields.iter().enumerate() {
let found = entries.iter().find(|(k, _)| k == fname);
let cell = match (
found,
def.field_user_types.get(i).and_then(Option::as_deref),
) {
(None, _) => Value::Null,
(Some((_, jv)), Some(tn)) => {
let inner_text = jv.to_json_text();
json_to_composite(&Value::Json(alloc::borrow::Cow::Owned(inner_text)), tn, ctx)
.unwrap_or(Value::Null)
}
(Some((_, jv)), None) => json_cell_to_value(jv, *fty),
};
fields.push((fname.clone(), cell));
}
Some(Value::Composite(fields))
}
fn json_cell_to_value(jv: &crate::json::JsonValue, ty: spg_storage::DataType) -> Value<'static> {
use crate::json::JsonValue as J;
let raw: Value<'static> = match jv {
J::Null => return Value::Null,
J::Bool(b) => Value::Bool(*b),
J::String(s) => Value::text(s.clone()),
J::Number(n) => Value::Float(*n),
J::NumberText(t) => Value::text(t.clone()),
other => Value::Json(alloc::borrow::Cow::Owned(other.to_json_text())),
};
crate::conversions::coerce_value(raw.clone(), ty, "", 0).unwrap_or(raw)
}