use crate::{
model::field::FieldKind,
types::{Account, Decimal, Float32, Float64, IntBig, NatBig, Principal, Ulid},
value::{Value, ValueEnum},
};
use std::str::FromStr;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FieldKindNumericClass {
Signed64,
Unsigned64,
SignedWide,
UnsignedWide,
FloatLike,
DecimalLike,
DurationLike,
TimestampLike,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FieldKindScalarClass {
Boolean,
Numeric(FieldKindNumericClass),
Text,
OrderedOpaque,
Opaque,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FieldKindCategory {
Scalar(FieldKindScalarClass),
Relation(FieldKindScalarClass),
Collection,
Structured { queryable: bool },
}
impl FieldKindCategory {
#[must_use]
const fn supports_aggregate_numeric(self) -> bool {
matches!(
self,
Self::Scalar(FieldKindScalarClass::Numeric(_))
| Self::Relation(FieldKindScalarClass::Numeric(_))
)
}
#[must_use]
const fn supports_aggregate_ordering(self) -> bool {
match self {
Self::Scalar(class) | Self::Relation(class) => scalar_class_supports_ordering(class),
Self::Collection | Self::Structured { .. } => false,
}
}
#[must_use]
const fn supports_predicate_numeric_widen(self) -> bool {
matches!(
self,
Self::Scalar(FieldKindScalarClass::Numeric(
FieldKindNumericClass::Signed64
| FieldKindNumericClass::Unsigned64
| FieldKindNumericClass::FloatLike
| FieldKindNumericClass::DecimalLike,
)) | Self::Relation(FieldKindScalarClass::Numeric(
FieldKindNumericClass::Signed64
| FieldKindNumericClass::Unsigned64
| FieldKindNumericClass::FloatLike
| FieldKindNumericClass::DecimalLike,
))
)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct FieldKindSemantics {
category: FieldKindCategory,
}
impl FieldKindSemantics {
#[must_use]
const fn new(category: FieldKindCategory) -> Self {
Self { category }
}
#[must_use]
const fn category(self) -> FieldKindCategory {
self.category
}
#[must_use]
pub(crate) const fn supports_aggregate_numeric(self) -> bool {
self.category.supports_aggregate_numeric()
}
#[must_use]
pub(crate) const fn supports_aggregate_ordering(self) -> bool {
self.category.supports_aggregate_ordering()
}
#[must_use]
pub(crate) const fn supports_predicate_numeric_widen(self) -> bool {
self.category.supports_predicate_numeric_widen()
}
}
#[must_use]
pub(crate) const fn field_kind_has_identity_group_canonical_form(kind: FieldKind) -> bool {
!matches!(
kind,
FieldKind::Decimal { .. }
| FieldKind::Enum { .. }
| FieldKind::Relation { .. }
| FieldKind::List(_)
| FieldKind::Set(_)
| FieldKind::Map { .. }
| FieldKind::Structured { .. }
| FieldKind::Unit
)
}
#[must_use]
pub(crate) fn canonicalize_grouped_having_numeric_literal_for_field_kind(
field_kind: Option<FieldKind>,
value: &Value,
) -> Option<Value> {
canonicalize_lossless_field_literal_for_kind(field_kind?, value, false)
}
#[cfg(any(test, feature = "sql"))]
#[must_use]
pub(crate) fn canonicalize_strict_sql_literal_for_kind(
kind: &FieldKind,
value: &Value,
) -> Option<Value> {
canonicalize_strict_sql_literal_for_kind_impl(*kind, value)
}
#[must_use]
pub(crate) fn canonicalize_filter_literal_for_kind(
kind: &FieldKind,
value: &Value,
) -> Option<Value> {
canonicalize_lossless_field_literal_for_kind(*kind, value, true)
}
#[must_use]
pub(crate) const fn classify_field_kind(kind: &FieldKind) -> FieldKindSemantics {
match kind {
FieldKind::Account
| FieldKind::Date
| FieldKind::Principal
| FieldKind::Subaccount
| FieldKind::Ulid
| FieldKind::Unit => FieldKindSemantics::new(FieldKindCategory::Scalar(
FieldKindScalarClass::OrderedOpaque,
)),
FieldKind::Blob { .. } => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Opaque))
}
FieldKind::Bool => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Boolean))
}
FieldKind::Decimal { .. } => FieldKindSemantics::new(FieldKindCategory::Scalar(
FieldKindScalarClass::Numeric(FieldKindNumericClass::DecimalLike),
)),
FieldKind::Duration => FieldKindSemantics::new(FieldKindCategory::Scalar(
FieldKindScalarClass::Numeric(FieldKindNumericClass::DurationLike),
)),
FieldKind::Int8 | FieldKind::Int16 | FieldKind::Int32 | FieldKind::Int64 => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Numeric(
FieldKindNumericClass::Signed64,
)))
}
FieldKind::Int128 | FieldKind::IntBig { .. } => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Numeric(
FieldKindNumericClass::SignedWide,
)))
}
FieldKind::Timestamp => FieldKindSemantics::new(FieldKindCategory::Scalar(
FieldKindScalarClass::Numeric(FieldKindNumericClass::TimestampLike),
)),
FieldKind::Nat8 | FieldKind::Nat16 | FieldKind::Nat32 | FieldKind::Nat64 => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Numeric(
FieldKindNumericClass::Unsigned64,
)))
}
FieldKind::Nat128 | FieldKind::NatBig { .. } => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Numeric(
FieldKindNumericClass::UnsignedWide,
)))
}
FieldKind::Enum { .. } | FieldKind::Text { .. } => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Text))
}
FieldKind::Float32 | FieldKind::Float64 => {
FieldKindSemantics::new(FieldKindCategory::Scalar(FieldKindScalarClass::Numeric(
FieldKindNumericClass::FloatLike,
)))
}
FieldKind::Relation { key_kind, .. } => FieldKindSemantics::new(
FieldKindCategory::Relation(classify_relation_scalar_class(key_kind)),
),
FieldKind::List(_) | FieldKind::Map { .. } | FieldKind::Set(_) => {
FieldKindSemantics::new(FieldKindCategory::Collection)
}
FieldKind::Structured { queryable } => {
FieldKindSemantics::new(FieldKindCategory::Structured {
queryable: *queryable,
})
}
}
}
const fn classify_relation_scalar_class(kind: &FieldKind) -> FieldKindScalarClass {
match classify_field_kind(kind).category() {
FieldKindCategory::Scalar(class) | FieldKindCategory::Relation(class) => class,
FieldKindCategory::Collection | FieldKindCategory::Structured { .. } => {
FieldKindScalarClass::Opaque
}
}
}
const fn scalar_class_supports_ordering(class: FieldKindScalarClass) -> bool {
!matches!(class, FieldKindScalarClass::Opaque)
}
#[expect(clippy::too_many_lines)]
fn canonicalize_lossless_field_literal_for_kind(
kind: FieldKind,
value: &Value,
allow_text_ulid: bool,
) -> Option<Value> {
match kind {
FieldKind::Account => match value {
Value::Account(inner) => Some(Value::Account(*inner)),
Value::Text(inner) => Account::from_str(inner).ok().map(Value::Account),
_ => None,
},
FieldKind::Bool => match value {
Value::Bool(inner) => Some(Value::Bool(*inner)),
_ => None,
},
FieldKind::Decimal { .. } => match value {
Value::Decimal(inner) => Some(Value::Decimal(*inner)),
Value::Text(inner) => Decimal::from_str(inner).ok().map(Value::Decimal),
_ => None,
},
FieldKind::Enum { .. } => match value {
Value::Enum(inner) => Some(Value::Enum(inner.clone())),
Value::Text(inner) => Some(Value::Enum(ValueEnum::loose(inner))),
_ => None,
},
FieldKind::Float32 => match value {
Value::Float32(inner) => Some(Value::Float32(*inner)),
Value::Text(inner) => inner
.parse::<f32>()
.ok()
.and_then(Float32::try_new)
.map(Value::Float32),
_ => None,
},
FieldKind::Float64 => match value {
Value::Float64(inner) => Some(Value::Float64(*inner)),
Value::Text(inner) => inner
.parse::<f64>()
.ok()
.and_then(Float64::try_new)
.map(Value::Float64),
_ => None,
},
FieldKind::Relation { key_kind, .. } => {
canonicalize_lossless_field_literal_for_kind(*key_kind, value, allow_text_ulid)
}
FieldKind::Int64 => canonicalize_int_literal(value, i64::MIN, i64::MAX),
FieldKind::Int8 => canonicalize_int_literal(value, i64::from(i8::MIN), i64::from(i8::MAX)),
FieldKind::Int16 => {
canonicalize_int_literal(value, i64::from(i16::MIN), i64::from(i16::MAX))
}
FieldKind::Int32 => {
canonicalize_int_literal(value, i64::from(i32::MIN), i64::from(i32::MAX))
}
FieldKind::Int128 => match value {
Value::Int128(inner) => Some(Value::Int128(*inner)),
Value::Text(inner) => inner.parse::<i128>().ok().map(Value::Int128),
_ => None,
},
FieldKind::IntBig { .. } => match value {
Value::IntBig(inner) => Some(Value::IntBig(inner.clone())),
Value::Text(inner) => IntBig::from_str(inner).ok().map(Value::IntBig),
_ => None,
},
FieldKind::List(inner) | FieldKind::Set(inner) => match value {
Value::List(values) => Some(Value::List(
values
.iter()
.map(|item| {
canonicalize_lossless_field_literal_for_kind(*inner, item, allow_text_ulid)
.unwrap_or_else(|| item.clone())
})
.collect(),
)),
_ => None,
},
FieldKind::Principal => match value {
Value::Principal(inner) => Some(Value::Principal(*inner)),
Value::Text(inner) => Principal::from_str(inner).ok().map(Value::Principal),
_ => None,
},
FieldKind::Text { .. } => match value {
Value::Text(inner) => Some(Value::Text(inner.clone())),
_ => None,
},
FieldKind::Nat64 => canonicalize_nat_literal(value, u64::MAX),
FieldKind::Nat8 => canonicalize_nat_literal(value, u64::from(u8::MAX)),
FieldKind::Nat16 => canonicalize_nat_literal(value, u64::from(u16::MAX)),
FieldKind::Nat32 => canonicalize_nat_literal(value, u64::from(u32::MAX)),
FieldKind::Nat128 => match value {
Value::Nat128(inner) => Some(Value::Nat128(*inner)),
Value::Text(inner) => inner.parse::<u128>().ok().map(Value::Nat128),
_ => None,
},
FieldKind::NatBig { .. } => match value {
Value::NatBig(inner) => Some(Value::NatBig(inner.clone())),
Value::Text(inner) => NatBig::from_str(inner).ok().map(Value::NatBig),
_ => None,
},
FieldKind::Unit => match value {
Value::Null | Value::Unit => Some(Value::Unit),
_ => None,
},
FieldKind::Ulid if allow_text_ulid => match value {
Value::Text(inner) => inner.parse::<Ulid>().ok().map(Value::Ulid),
Value::Ulid(inner) => Some(Value::Ulid(*inner)),
_ => None,
},
_ => None,
}
}
fn canonicalize_int_literal(value: &Value, min: i64, max: i64) -> Option<Value> {
let value = match value {
Value::Int64(inner) => *inner,
Value::Nat64(inner) => i64::try_from(*inner).ok()?,
Value::Text(inner) => inner.parse::<i64>().ok()?,
_ => return None,
};
(min..=max).contains(&value).then_some(Value::Int64(value))
}
fn canonicalize_nat_literal(value: &Value, max: u64) -> Option<Value> {
let value = match value {
Value::Int64(inner) => u64::try_from(*inner).ok()?,
Value::Nat64(inner) => *inner,
Value::Text(inner) => inner.parse::<u64>().ok()?,
_ => return None,
};
(value <= max).then_some(Value::Nat64(value))
}
#[cfg(any(test, feature = "sql"))]
fn canonicalize_int_strict_literal(value: &Value, min: i64, max: i64) -> Option<Value> {
let value = match value {
Value::Int64(inner) => *inner,
Value::Nat64(inner) => i64::try_from(*inner).ok()?,
_ => return None,
};
(min..=max).contains(&value).then_some(Value::Int64(value))
}
#[cfg(any(test, feature = "sql"))]
fn canonicalize_nat_strict_literal(value: &Value, max: u64) -> Option<Value> {
let value = match value {
Value::Int64(inner) => u64::try_from(*inner).ok()?,
Value::Nat64(inner) => *inner,
_ => return None,
};
(value <= max).then_some(Value::Nat64(value))
}
#[cfg(any(test, feature = "sql"))]
fn canonicalize_strict_sql_literal_for_kind_impl(kind: FieldKind, value: &Value) -> Option<Value> {
match kind {
FieldKind::Relation { key_kind, .. } => {
canonicalize_strict_sql_literal_for_kind_impl(*key_kind, value)
}
FieldKind::Int64 => canonicalize_int_strict_literal(value, i64::MIN, i64::MAX),
FieldKind::Int8 => {
canonicalize_int_strict_literal(value, i64::from(i8::MIN), i64::from(i8::MAX))
}
FieldKind::Int16 => {
canonicalize_int_strict_literal(value, i64::from(i16::MIN), i64::from(i16::MAX))
}
FieldKind::Int32 => {
canonicalize_int_strict_literal(value, i64::from(i32::MIN), i64::from(i32::MAX))
}
FieldKind::Nat64 => canonicalize_nat_strict_literal(value, u64::MAX),
FieldKind::Nat8 => canonicalize_nat_strict_literal(value, u64::from(u8::MAX)),
FieldKind::Nat16 => canonicalize_nat_strict_literal(value, u64::from(u16::MAX)),
FieldKind::Nat32 => canonicalize_nat_strict_literal(value, u64::from(u32::MAX)),
FieldKind::Ulid => match value {
Value::Text(inner) => inner.parse::<Ulid>().ok().map(Value::Ulid),
_ => None,
},
FieldKind::List(inner) | FieldKind::Set(inner) => match value {
Value::List(values) => values
.iter()
.map(|item| canonicalize_strict_sql_literal_for_kind_impl(*inner, item))
.collect::<Option<Vec<_>>>()
.map(Value::List),
_ => None,
},
_ => None,
}
}
#[cfg(test)]
mod tests;