use std::collections::BTreeSet;
use uqa_core::{DocId, Value};
use uqa_sql::{ast::BinaryOp, ast::ColumnDef, SQLParam, ScalarExpr};
#[derive(Clone, Copy)]
pub(crate) struct IdentityColumns<'a> {
document_id: bool,
key: Option<&'a str>,
}
impl<'a> IdentityColumns<'a> {
pub(crate) fn new(
columns: &'a [ColumnDef],
maps_integer_keys: bool,
visible: impl FnOnce(&'a str) -> &'a str,
) -> Self {
Self {
document_id: !columns
.iter()
.any(|column| column.name == uqa_sql::semantics::DOC_ID_COLUMN),
key: maps_integer_keys
.then(|| {
columns
.iter()
.find(|column| column.primary_key && column.ty.is_integer())
})
.flatten()
.map(|column| visible(column.name.as_str())),
}
}
}
pub(crate) fn key_candidates(
filter: &ScalarExpr,
params: &[SQLParam],
columns: IdentityColumns<'_>,
) -> Option<Vec<DocId>> {
candidates(filter, params, columns).map(|identities| identities.into_iter().collect())
}
fn candidates(
filter: &ScalarExpr,
params: &[SQLParam],
columns: IdentityColumns<'_>,
) -> Option<BTreeSet<DocId>> {
if let Some(membership) = uqa_sql::semantics::membership_operands(filter) {
if membership.negated {
return None;
}
let column = identity_column(membership.value, columns)?;
let mut identities = BTreeSet::new();
for item in membership.items.iter() {
let value = match item {
uqa_sql::semantics::MembershipItem::Expression(expression) => {
constant(expression, params)?
}
uqa_sql::semantics::MembershipItem::Constant(value) => value.clone(),
};
identities.extend(column.identities(&value)?);
}
return Some(identities);
}
match filter {
ScalarExpr::And(parts) => parts
.iter()
.filter_map(|part| candidates(part, params, columns))
.reduce(|left, right| left.intersection(&right).copied().collect()),
ScalarExpr::Binary {
op: BinaryOp::Equal,
lhs,
rhs,
} => {
let (column, value) =
match (identity_column(lhs, columns), identity_column(rhs, columns)) {
(Some(column), None) => (column, constant(rhs, params)?),
(None, Some(column)) => (column, constant(lhs, params)?),
_ => return None,
};
column
.identities(&value)
.map(|identities| identities.into_iter().collect())
}
_ => None,
}
}
#[derive(Clone, Copy)]
enum IdentityColumn {
DocumentId,
Key,
}
impl IdentityColumn {
fn identities(self, value: &Value) -> Option<Vec<DocId>> {
match (self, value) {
(Self::DocumentId, Value::Int(value)) => {
Some(DocId::try_from(*value).into_iter().collect())
}
(Self::Key, value) => {
uqa_sql::semantics::key_identity::key_document_id(value).map(|id| vec![id])
}
_ => None,
}
}
}
fn identity_column(
expression: &ScalarExpr,
columns: IdentityColumns<'_>,
) -> Option<IdentityColumn> {
let (ScalarExpr::Column(column) | ScalarExpr::QualifiedColumn { column, .. }) = expression
else {
return None;
};
if columns.document_id && column == uqa_sql::semantics::DOC_ID_COLUMN {
Some(IdentityColumn::DocumentId)
} else if columns.key == Some(column.as_str()) {
Some(IdentityColumn::Key)
} else {
None
}
}
fn constant(expression: &ScalarExpr, params: &[SQLParam]) -> Option<Value> {
if !uqa_sql::semantics::mutation_inputs::expr_is_row_independent(expression) {
return None;
}
crate::eval_scalar(expression, &crate::ScalarEvalContext::new(None, params)).ok()
}
#[cfg(test)]
mod tests;