use std::collections::HashSet;
use topcoat::{Result, context::Cx};
use crate::{
db::db,
form::{FieldErrors, FormValues},
resource::{Mounted, Resource, Trashed},
};
pub(super) async fn check_unique<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
schema: &crate::schema::Schema,
values: &FormValues,
current: &FormValues,
ex: &mut dyn toasty::Executor,
) -> Result<FieldErrors, topcoat::Error> {
let mut errors = FieldErrors::new();
let skip = schema.hidden_fields(values);
let mut probed = HashSet::new();
for field in schema.fields() {
let name = field.name();
if !field.is_unique() || skip.contains(name) {
continue;
}
for others in field.unique_with() {
let mut members: Vec<&str> = others
.iter()
.map(String::as_str)
.filter(|other| schema.fields().any(|field| field.name() == *other))
.chain([name])
.collect();
members.sort_unstable();
if probed.contains(&members) {
continue;
}
let Some(filter) = probe(schema, &skip, values, current, &members) else {
continue;
};
probed.insert(members);
let rows = resource
.scoped_query_in(cx, Trashed::With)?
.filter(filter)
.limit(1)
.exec(&mut *ex)
.await
.map_err(crate::error::unavailable)?;
if !rows.is_empty() {
errors.add(
name,
crate::text!("tablo-field-taken", label = field.label()),
);
break;
}
}
}
Ok(errors)
}
pub(super) async fn taken_since<R: Resource>(
cx: &Cx,
resource: &Mounted<R>,
schema: &crate::schema::Schema,
values: &FormValues,
current: &FormValues,
error: &topcoat::Error,
) -> FieldErrors {
if !error.is::<toasty::Error>() {
return FieldErrors::new();
}
let Ok(mut db) = db(cx) else {
return FieldErrors::new();
};
check_unique(cx, resource, schema, values, current, &mut db)
.await
.unwrap_or_default()
}
fn probe(
schema: &crate::schema::Schema,
skip: &HashSet<String>,
values: &FormValues,
current: &FormValues,
members: &[&str],
) -> Option<toasty::stmt::Expr<bool>> {
let mut filter: Option<toasty::stmt::Expr<bool>> = None;
let mut unchanged = true;
for member in members {
let field = schema.fields().find(|field| field.name() == *member)?;
if skip.contains(*member) {
return None;
}
let submitted = values
.text(member)
.map(str::trim)
.filter(|s| !s.is_empty())?;
unchanged &= current
.text(member)
.is_some_and(|kept| field.same_value(kept, submitted));
let eq = field.eq_filter(submitted)?;
filter = Some(match filter {
Some(filter) => filter.and(eq),
None => eq,
});
}
filter.filter(|_| !unchanged)
}
#[cfg(test)]
mod tests;