use toasty::{
schema::app::{FieldId, FieldTy},
stmt::{IntoExpr, Type, Value},
};
use toasty_core::stmt::Expr;
use super::value_text;
fn pk_fields<M>() -> Vec<FieldId>
where
M: toasty::schema::Model,
{
M::schema()
.as_root()
.map(|root| root.primary_key.fields.clone())
.unwrap_or_default()
}
fn pk_values<M>(record: &M) -> Vec<Value>
where
M: toasty::schema::Model + IntoExpr<M>,
{
let values = match Expr::from(record.by_ref()) {
Expr::Value(Value::Record(record)) => record.fields,
Expr::Record(record) => record
.fields
.into_iter()
.map(|field| match field {
Expr::Value(value) => Some(value),
_ => None,
})
.collect::<Option<_>>()
.unwrap_or_default(),
Expr::Value(value) => vec![value],
_ => Vec::new(),
};
if values.len() == pk_fields::<M>().len() {
values
} else {
Vec::new()
}
}
pub(crate) fn pk_text<M>(record: &M) -> String
where
M: toasty::schema::Model + IntoExpr<M>,
{
pk_values(record)
.iter()
.map(|value| value_text(value).unwrap_or_else(|| format!("{value:?}")))
.collect::<Vec<_>>()
.join(",")
}
pub(crate) fn pk_filter<M>(record: &M) -> toasty::stmt::Expr<bool>
where
M: toasty::schema::Model + IntoExpr<M>,
{
let operands = pk_fields::<M>()
.into_iter()
.zip(pk_values(record))
.map(|(field, value)| Expr::eq(Expr::ref_self_field(field), Expr::from(value)))
.collect::<Vec<_>>();
if operands.is_empty() {
return toasty::stmt::Expr::from_untyped(Expr::from(false));
}
toasty::stmt::Expr::from_untyped(Expr::and_from_vec(operands))
}
pub(crate) fn pk_expr<M, T>(record: &M) -> toasty::stmt::Expr<T>
where
M: toasty::schema::Model + IntoExpr<M>,
{
toasty::stmt::Expr::from_untyped(record.by_ref())
}
fn pk_field_value<M>(id: &str) -> Option<(FieldId, Value)>
where
M: toasty::schema::Model,
{
let app_model = M::schema();
let root = app_model.as_root()?;
if root.primary_key.fields.len() != 1 {
return None;
}
let fid = root.primary_key.fields.first().copied()?;
let model_field = app_model.fields().get(fid.index)?;
let FieldTy::Primitive(prim) = &model_field.ty else {
return None;
};
let value = match prim.ty {
Type::Uuid => Value::Uuid(id.parse().ok()?),
Type::String => Value::String(id.to_string()),
Type::Bool => Value::Bool(id.parse().ok()?),
Type::I8 => Value::I8(id.parse().ok()?),
Type::I16 => Value::I16(id.parse().ok()?),
Type::I32 => Value::I32(id.parse().ok()?),
Type::I64 => Value::I64(id.parse().ok()?),
Type::U8 => Value::U8(id.parse().ok()?),
Type::U16 => Value::U16(id.parse().ok()?),
Type::U32 => Value::U32(id.parse().ok()?),
Type::U64 => Value::U64(id.parse().ok()?),
Type::F32 => Value::F32(id.parse().ok()?),
Type::F64 => Value::F64(id.parse().ok()?),
Type::Timestamp => Value::Timestamp(id.parse().ok()?),
Type::Date => Value::Date(id.parse().ok()?),
Type::Time => Value::Time(id.parse().ok()?),
Type::DateTime => Value::DateTime(id.parse().ok()?),
Type::Zoned => Value::Zoned(id.parse().ok()?),
Type::Bytes => Value::Bytes(id.as_bytes().to_vec()),
_ => return None,
};
Some((fid, value))
}
pub(crate) fn pk_order_bys<M>() -> Vec<toasty::stmt::OrderByExpr>
where
M: toasty::schema::Model,
{
debug_assert!(
M::schema().as_root().is_some(),
"pk_order_bys: {} is not a root model; deterministic pagination needs its primary key",
std::any::type_name::<M>()
);
pk_fields::<M>()
.iter()
.map(|fid| M::path_field::<toasty::stmt::Value>(fid.index).asc())
.collect()
}
pub(crate) fn pk_is_composite<M>() -> bool
where
M: toasty::schema::Model,
{
pk_fields::<M>().len() > 1
}
pub(crate) fn pk_eq_expr<M>(id: &str) -> Option<toasty::stmt::Expr<bool>>
where
M: toasty::schema::Model,
{
let (fid, value) = pk_field_value::<M>(id)?;
let cond = Expr::eq(Expr::ref_self_field(fid), Expr::from(value));
Some(toasty::stmt::Expr::from_untyped(cond))
}
pub(crate) fn pk_in_expr<M>(ids: &[&str]) -> Option<toasty::stmt::Expr<bool>>
where
M: toasty::schema::Model,
{
let mut parsed = ids.iter().map(|id| pk_field_value::<M>(id));
let (fid, first) = parsed.next()??;
let mut values = vec![first];
for item in parsed {
let (f, v) = item?;
debug_assert_eq!(
f.index, fid.index,
"pk_in_expr: one model, one PK field — mixed fields are a bug"
);
values.push(v);
}
let cond = Expr::in_list(Expr::ref_self_field(fid), Expr::list(values));
Some(toasty::stmt::Expr::from_untyped(cond))
}
#[cfg(test)]
mod tests;