use super::ColumnType;
use crate::Columns;
use crate::columns::{Message, column_for, str_eq};
use queryweave::{PersistedFields, ValueKind, ValueType};
#[mutants::skip]
pub const fn assert_condition_types<E: Columns + ?Sized, M: PersistedFields>(
columns: &[(&str, &[ValueKind])],
) {
assert_value_types::<E, M>(Target::Filter(columns));
}
#[mutants::skip]
pub const fn assert_assignment_types<E: Columns + ?Sized, M: PersistedFields>(
columns: &[(&str, ColumnType)],
) {
assert_value_types::<E, M>(Target::Write(columns));
}
#[allow(
clippy::panic,
reason = "a const-evaluation panic is how a compile-time check fails the build"
)]
const fn assert_value_types<E: Columns + ?Sized, M: PersistedFields>(target: Target<'_>) {
if let Some(message) =
value_type_mismatch(E::NAME, E::COLUMNS, target, M::FIELD_NAMES, M::FIELD_TYPES)
{
panic!("{}", message.as_str());
}
}
#[derive(Clone, Copy)]
enum Target<'c> {
Filter(&'c [(&'c str, &'c [ValueKind])]),
Write(&'c [(&'c str, ColumnType)]),
}
const fn value_type_mismatch(
entity: &str,
names: &'static [(&'static str, &'static str)],
target: Target<'_>,
fields: &[&str],
types: &[ValueType],
) -> Option<Message> {
if types.len() != fields.len() {
return None;
}
let mut field = 0;
while field < fields.len() {
let (name, value_type) = (fields[field], types[field]);
if let Some((accepted, nullable)) = column_type(target, name) {
let column = match column_for(names, name) {
Some(column) => column,
None => name,
};
if !accepts(accepted, value_type.kind) {
return Some(kind_message(
entity,
name,
column,
value_type.kind,
accepted,
));
}
if value_type.nullable && !nullable {
return Some(nullable_message(entity, name, column));
}
}
field += 1;
}
None
}
const fn column_type<'c>(target: Target<'c>, field: &str) -> Option<(&'c [ValueKind], bool)> {
let mut index = 0;
match target {
Target::Filter(columns) => {
while index < columns.len() {
let (domain_field, accepts) = columns[index];
if str_eq(domain_field, field) {
return Some((accepts, true));
}
index += 1;
}
},
Target::Write(columns) => {
while index < columns.len() {
let (domain_field, column_type) = columns[index];
if str_eq(domain_field, field) {
return Some((column_type.accepts, column_type.nullable));
}
index += 1;
}
},
}
None
}
const fn accepts(accepts: &[ValueKind], kind: ValueKind) -> bool {
let mut index = 0;
while index < accepts.len() {
if accepts[index] as u8 == kind as u8 {
return true;
}
index += 1;
}
false
}
const fn kind_message(
entity: &str,
field: &str,
column: &str,
kind: ValueKind,
accepts: &[ValueKind],
) -> Message {
let mut message = Message::new()
.push("the value of domain field `")
.push(field)
.push("` (`")
.push(kind.rust_type())
.push("`) does not fit the column `")
.push(column)
.push("` of `")
.push(entity)
.push("`, which takes ");
let mut index = 0;
while index < accepts.len() {
if index > 0 {
message = message.push(" or ");
}
message = message.push("`").push(accepts[index].rust_type()).push("`");
index += 1;
}
message.push(": make the field's type and the column's SQL type in `table!` agree")
}
const fn nullable_message(entity: &str, field: &str, column: &str) -> Message {
Message::new()
.push("domain field `")
.push(field)
.push("` may be `None`, but the column `")
.push(column)
.push("` of `")
.push(entity)
.push("` is `NOT NULL`: make the column `Nullable` in `table!`, or the field not optional")
}
#[cfg(test)]
mod tests {
use super::{ColumnType, Target, value_type_mismatch};
use pretty_assertions::assert_eq;
use queryweave::{ValueKind, ValueType};
const NAMES: &[(&str, &str)] = &[("quantity", "quantity"), ("delivery_date", "delivered_at")];
const COLUMNS: &[(&str, ColumnType)] = &[
(
"quantity",
ColumnType {
accepts: &[ValueKind::I32, ValueKind::I16],
nullable: false,
},
),
(
"delivery_date",
ColumnType {
accepts: &[ValueKind::DateTimeUtc],
nullable: false,
},
),
];
const FILTER_COLUMNS: &[(&str, &[ValueKind])] = &[
("quantity", &[ValueKind::I32, ValueKind::I16]),
("delivery_date", &[ValueKind::DateTimeUtc]),
];
const FIELDS: &[&str] = &["quantity", "delivery_date"];
const fn value(kind: ValueKind, nullable: bool) -> ValueType {
ValueType { kind, nullable }
}
fn mismatch(types: &[ValueType], writes: bool) -> Option<String> {
let target = if writes {
Target::Write(COLUMNS)
} else {
Target::Filter(FILTER_COLUMNS)
};
value_type_mismatch("Order", NAMES, target, FIELDS, types)
.map(|message| message.as_str().to_owned())
}
#[test]
fn values_of_a_kind_the_column_takes_fit() {
let types = [
value(ValueKind::I16, false),
value(ValueKind::DateTimeUtc, false),
];
assert_eq!(mismatch(&types, false), None);
assert_eq!(mismatch(&types, true), None);
}
#[test]
fn a_value_of_another_kind_names_the_field_column_and_kinds() {
let types = [
value(ValueKind::Text, false),
value(ValueKind::DateTimeUtc, false),
];
assert_eq!(
mismatch(&types, false).as_deref(),
Some(
"the value of domain field `quantity` (`String`) does not fit the column `quantity` of `Order`, which takes `i32` or `i16`: make the field's type and the column's SQL type in `table!` agree"
)
);
}
#[test]
fn a_nullable_value_needs_a_nullable_column_only_to_be_written() {
let types = [
value(ValueKind::I16, false),
value(ValueKind::DateTimeUtc, true),
];
assert_eq!(mismatch(&types, false), None);
assert_eq!(
mismatch(&types, true).as_deref(),
Some(
"domain field `delivery_date` may be `None`, but the column `delivered_at` of `Order` is `NOT NULL`: make the column `Nullable` in `table!`, or the field not optional"
)
);
}
#[test]
fn a_model_without_value_types_is_not_checked() {
assert_eq!(mismatch(&[], true), None);
}
}