use crate::{traits::RuntimeValueKind, types::EntityTag, value::Value};
use std::cmp::Ordering;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldStorageDecode {
ByKind,
Value,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScalarCodec {
Blob,
Bool,
Date,
Duration,
Float32,
Float64,
Int64,
Principal,
Subaccount,
Text,
Timestamp,
Uint64,
Ulid,
Unit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LeafCodec {
Scalar(ScalarCodec),
StructuralFallback,
}
#[derive(Clone, Copy, Debug)]
pub struct EnumVariantModel {
pub(crate) ident: &'static str,
pub(crate) payload_kind: Option<&'static FieldKind>,
pub(crate) payload_storage_decode: FieldStorageDecode,
}
impl EnumVariantModel {
#[must_use]
pub const fn new(
ident: &'static str,
payload_kind: Option<&'static FieldKind>,
payload_storage_decode: FieldStorageDecode,
) -> Self {
Self {
ident,
payload_kind,
payload_storage_decode,
}
}
#[must_use]
pub const fn ident(&self) -> &'static str {
self.ident
}
#[must_use]
pub const fn payload_kind(&self) -> Option<&'static FieldKind> {
self.payload_kind
}
#[must_use]
pub const fn payload_storage_decode(&self) -> FieldStorageDecode {
self.payload_storage_decode
}
}
#[derive(Debug)]
pub struct FieldModel {
pub(crate) name: &'static str,
pub(crate) kind: FieldKind,
pub(crate) nullable: bool,
pub(crate) storage_decode: FieldStorageDecode,
pub(crate) leaf_codec: LeafCodec,
pub(crate) insert_generation: Option<FieldInsertGeneration>,
pub(crate) write_management: Option<FieldWriteManagement>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldInsertGeneration {
Ulid,
Timestamp,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FieldWriteManagement {
CreatedAt,
UpdatedAt,
}
impl FieldModel {
#[must_use]
#[doc(hidden)]
pub const fn generated(name: &'static str, kind: FieldKind) -> Self {
Self::generated_with_storage_decode_and_nullability(
name,
kind,
FieldStorageDecode::ByKind,
false,
)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
) -> Self {
Self::generated_with_storage_decode_and_nullability(name, kind, storage_decode, false)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_and_nullability(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
) -> Self {
Self::generated_with_storage_decode_nullability_and_write_policies(
name,
kind,
storage_decode,
nullable,
None,
None,
)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_nullability_and_insert_generation(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
insert_generation: Option<FieldInsertGeneration>,
) -> Self {
Self::generated_with_storage_decode_nullability_and_write_policies(
name,
kind,
storage_decode,
nullable,
insert_generation,
None,
)
}
#[must_use]
#[doc(hidden)]
pub const fn generated_with_storage_decode_nullability_and_write_policies(
name: &'static str,
kind: FieldKind,
storage_decode: FieldStorageDecode,
nullable: bool,
insert_generation: Option<FieldInsertGeneration>,
write_management: Option<FieldWriteManagement>,
) -> Self {
Self {
name,
kind,
nullable,
storage_decode,
leaf_codec: leaf_codec_for(kind, storage_decode),
insert_generation,
write_management,
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn kind(&self) -> FieldKind {
self.kind
}
#[must_use]
pub const fn nullable(&self) -> bool {
self.nullable
}
#[must_use]
pub const fn storage_decode(&self) -> FieldStorageDecode {
self.storage_decode
}
#[must_use]
pub const fn leaf_codec(&self) -> LeafCodec {
self.leaf_codec
}
#[must_use]
pub const fn insert_generation(&self) -> Option<FieldInsertGeneration> {
self.insert_generation
}
#[must_use]
pub const fn write_management(&self) -> Option<FieldWriteManagement> {
self.write_management
}
pub(crate) fn validate_runtime_value_for_storage(&self, value: &Value) -> Result<(), String> {
if matches!(value, Value::Null) {
if self.nullable() {
return Ok(());
}
return Err("required field cannot store null".into());
}
let accepts = match self.storage_decode() {
FieldStorageDecode::Value => {
value_storage_kind_accepts_runtime_value(self.kind(), value)
}
FieldStorageDecode::ByKind => {
by_kind_storage_kind_accepts_runtime_value(self.kind(), value)
}
};
if !accepts {
return Err(format!(
"field kind {:?} does not accept runtime value {value:?}",
self.kind()
));
}
ensure_decimal_scale_matches(self.kind(), value)?;
ensure_value_is_deterministic_for_storage(self.kind(), value)
}
}
const fn leaf_codec_for(kind: FieldKind, storage_decode: FieldStorageDecode) -> LeafCodec {
if matches!(storage_decode, FieldStorageDecode::Value) {
return LeafCodec::StructuralFallback;
}
match kind {
FieldKind::Blob => LeafCodec::Scalar(ScalarCodec::Blob),
FieldKind::Bool => LeafCodec::Scalar(ScalarCodec::Bool),
FieldKind::Date => LeafCodec::Scalar(ScalarCodec::Date),
FieldKind::Duration => LeafCodec::Scalar(ScalarCodec::Duration),
FieldKind::Float32 => LeafCodec::Scalar(ScalarCodec::Float32),
FieldKind::Float64 => LeafCodec::Scalar(ScalarCodec::Float64),
FieldKind::Int => LeafCodec::Scalar(ScalarCodec::Int64),
FieldKind::Principal => LeafCodec::Scalar(ScalarCodec::Principal),
FieldKind::Subaccount => LeafCodec::Scalar(ScalarCodec::Subaccount),
FieldKind::Text => LeafCodec::Scalar(ScalarCodec::Text),
FieldKind::Timestamp => LeafCodec::Scalar(ScalarCodec::Timestamp),
FieldKind::Uint => LeafCodec::Scalar(ScalarCodec::Uint64),
FieldKind::Ulid => LeafCodec::Scalar(ScalarCodec::Ulid),
FieldKind::Unit => LeafCodec::Scalar(ScalarCodec::Unit),
FieldKind::Relation { key_kind, .. } => leaf_codec_for(*key_kind, storage_decode),
FieldKind::Account
| FieldKind::Decimal { .. }
| FieldKind::Enum { .. }
| FieldKind::Int128
| FieldKind::IntBig
| FieldKind::List(_)
| FieldKind::Map { .. }
| FieldKind::Set(_)
| FieldKind::Structured { .. }
| FieldKind::Uint128
| FieldKind::UintBig => LeafCodec::StructuralFallback,
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RelationStrength {
Strong,
Weak,
}
#[derive(Clone, Copy, Debug)]
pub enum FieldKind {
Account,
Blob,
Bool,
Date,
Decimal {
scale: u32,
},
Duration,
Enum {
path: &'static str,
variants: &'static [EnumVariantModel],
},
Float32,
Float64,
Int,
Int128,
IntBig,
Principal,
Subaccount,
Text,
Timestamp,
Uint,
Uint128,
UintBig,
Ulid,
Unit,
Relation {
target_path: &'static str,
target_entity_name: &'static str,
target_entity_tag: EntityTag,
target_store_path: &'static str,
key_kind: &'static Self,
strength: RelationStrength,
},
List(&'static Self),
Set(&'static Self),
Map {
key: &'static Self,
value: &'static Self,
},
Structured {
queryable: bool,
},
}
impl FieldKind {
#[must_use]
pub const fn value_kind(&self) -> RuntimeValueKind {
match self {
Self::Account
| Self::Blob
| Self::Bool
| Self::Date
| Self::Duration
| Self::Enum { .. }
| Self::Float32
| Self::Float64
| Self::Int
| Self::Int128
| Self::IntBig
| Self::Principal
| Self::Subaccount
| Self::Text
| Self::Timestamp
| Self::Uint
| Self::Uint128
| Self::UintBig
| Self::Ulid
| Self::Unit
| Self::Decimal { .. }
| Self::Relation { .. } => RuntimeValueKind::Atomic,
Self::List(_) | Self::Set(_) => RuntimeValueKind::Structured { queryable: true },
Self::Map { .. } => RuntimeValueKind::Structured { queryable: false },
Self::Structured { queryable } => RuntimeValueKind::Structured {
queryable: *queryable,
},
}
}
#[must_use]
pub const fn is_deterministic_collection_shape(&self) -> bool {
match self {
Self::Relation { key_kind, .. } => key_kind.is_deterministic_collection_shape(),
Self::List(inner) | Self::Set(inner) => inner.is_deterministic_collection_shape(),
Self::Map { key, value } => {
key.is_deterministic_collection_shape() && value.is_deterministic_collection_shape()
}
_ => true,
}
}
#[must_use]
pub(crate) fn supports_group_probe(&self) -> bool {
match self {
Self::Enum { variants, .. } => variants.iter().all(|variant| {
variant
.payload_kind()
.is_none_or(Self::supports_group_probe)
}),
Self::Relation { key_kind, .. } => key_kind.supports_group_probe(),
Self::List(_)
| Self::Set(_)
| Self::Map { .. }
| Self::Structured { .. }
| Self::Unit => false,
Self::Account
| Self::Blob
| Self::Bool
| Self::Date
| Self::Decimal { .. }
| Self::Duration
| Self::Float32
| Self::Float64
| Self::Int
| Self::Int128
| Self::IntBig
| Self::Principal
| Self::Subaccount
| Self::Text
| Self::Timestamp
| Self::Uint
| Self::Uint128
| Self::UintBig
| Self::Ulid => true,
}
}
#[must_use]
pub(crate) fn accepts_value(&self, value: &Value) -> bool {
match (self, value) {
(Self::Account, Value::Account(_))
| (Self::Blob, Value::Blob(_))
| (Self::Bool, Value::Bool(_))
| (Self::Date, Value::Date(_))
| (Self::Decimal { .. }, Value::Decimal(_))
| (Self::Duration, Value::Duration(_))
| (Self::Enum { .. }, Value::Enum(_))
| (Self::Float32, Value::Float32(_))
| (Self::Float64, Value::Float64(_))
| (Self::Int, Value::Int(_))
| (Self::Int128, Value::Int128(_))
| (Self::IntBig, Value::IntBig(_))
| (Self::Principal, Value::Principal(_))
| (Self::Subaccount, Value::Subaccount(_))
| (Self::Text, Value::Text(_))
| (Self::Timestamp, Value::Timestamp(_))
| (Self::Uint, Value::Uint(_))
| (Self::Uint128, Value::Uint128(_))
| (Self::UintBig, Value::UintBig(_))
| (Self::Ulid, Value::Ulid(_))
| (Self::Unit, Value::Unit)
| (Self::Structured { .. }, Value::List(_) | Value::Map(_)) => true,
(Self::Relation { key_kind, .. }, value) => key_kind.accepts_value(value),
(Self::List(inner) | Self::Set(inner), Value::List(items)) => {
items.iter().all(|item| inner.accepts_value(item))
}
(Self::Map { key, value }, Value::Map(entries)) => {
if Value::validate_map_entries(entries.as_slice()).is_err() {
return false;
}
entries.iter().all(|(entry_key, entry_value)| {
key.accepts_value(entry_key) && value.accepts_value(entry_value)
})
}
_ => false,
}
}
}
fn by_kind_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
match (kind, value) {
(FieldKind::Relation { key_kind, .. }, value) => {
by_kind_storage_kind_accepts_runtime_value(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
.iter()
.all(|item| by_kind_storage_kind_accepts_runtime_value(*inner, item)),
(
FieldKind::Map {
key,
value: value_kind,
},
Value::Map(entries),
) => {
if Value::validate_map_entries(entries.as_slice()).is_err() {
return false;
}
entries.iter().all(|(entry_key, entry_value)| {
by_kind_storage_kind_accepts_runtime_value(*key, entry_key)
&& by_kind_storage_kind_accepts_runtime_value(*value_kind, entry_value)
})
}
(FieldKind::Structured { .. }, _) => false,
_ => kind.accepts_value(value),
}
}
fn value_storage_kind_accepts_runtime_value(kind: FieldKind, value: &Value) -> bool {
match (kind, value) {
(FieldKind::Structured { .. }, _) => true,
(FieldKind::Relation { key_kind, .. }, value) => {
value_storage_kind_accepts_runtime_value(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => items
.iter()
.all(|item| value_storage_kind_accepts_runtime_value(*inner, item)),
(
FieldKind::Map {
key,
value: value_kind,
},
Value::Map(entries),
) => {
if Value::validate_map_entries(entries.as_slice()).is_err() {
return false;
}
entries.iter().all(|(entry_key, entry_value)| {
value_storage_kind_accepts_runtime_value(*key, entry_key)
&& value_storage_kind_accepts_runtime_value(*value_kind, entry_value)
})
}
_ => kind.accepts_value(value),
}
}
fn ensure_decimal_scale_matches(kind: FieldKind, value: &Value) -> Result<(), String> {
if matches!(value, Value::Null) {
return Ok(());
}
match (kind, value) {
(FieldKind::Decimal { scale }, Value::Decimal(decimal)) => {
if decimal.scale() != scale {
return Err(format!(
"decimal scale mismatch: expected {scale}, found {}",
decimal.scale()
));
}
Ok(())
}
(FieldKind::Relation { key_kind, .. }, value) => {
ensure_decimal_scale_matches(*key_kind, value)
}
(FieldKind::List(inner) | FieldKind::Set(inner), Value::List(items)) => {
for item in items {
ensure_decimal_scale_matches(*inner, item)?;
}
Ok(())
}
(
FieldKind::Map {
key,
value: map_value,
},
Value::Map(entries),
) => {
for (entry_key, entry_value) in entries {
ensure_decimal_scale_matches(*key, entry_key)?;
ensure_decimal_scale_matches(*map_value, entry_value)?;
}
Ok(())
}
_ => Ok(()),
}
}
fn ensure_value_is_deterministic_for_storage(kind: FieldKind, value: &Value) -> Result<(), String> {
match (kind, value) {
(FieldKind::Set(_), Value::List(items)) => {
for pair in items.windows(2) {
let [left, right] = pair else {
continue;
};
if Value::canonical_cmp(left, right) != Ordering::Less {
return Err("set payload must already be canonical and deduplicated".into());
}
}
Ok(())
}
(FieldKind::Map { .. }, Value::Map(entries)) => {
Value::validate_map_entries(entries.as_slice()).map_err(|err| err.to_string())?;
if !Value::map_entries_are_strictly_canonical(entries.as_slice()) {
return Err("map payload must already be canonical and deduplicated".into());
}
Ok(())
}
_ => Ok(()),
}
}