use crate::{
db::schema::{
FieldId, SchemaFieldSlot, SchemaRowLayout, SchemaVersion, schema_snapshot_integrity_detail,
},
error::InternalError,
model::field::{
FieldDatabaseDefault, FieldKind, FieldStorageDecode, LeafCodec, RelationStrength,
},
types::EntityTag,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct AcceptedSchemaSnapshot {
snapshot: PersistedSchemaSnapshot,
}
impl AcceptedSchemaSnapshot {
pub(in crate::db) fn try_new(snapshot: PersistedSchemaSnapshot) -> Result<Self, InternalError> {
if let Some(detail) = schema_snapshot_integrity_detail(
"accepted schema snapshot",
snapshot.version(),
snapshot.primary_key_field_id(),
snapshot.row_layout(),
snapshot.fields(),
) {
return Err(InternalError::store_invariant(detail));
}
Ok(Self { snapshot })
}
#[must_use]
#[cfg(test)]
pub(in crate::db) const fn new(snapshot: PersistedSchemaSnapshot) -> Self {
Self { snapshot }
}
#[must_use]
pub(in crate::db::schema) const fn persisted_snapshot(&self) -> &PersistedSchemaSnapshot {
&self.snapshot
}
#[must_use]
pub(in crate::db) const fn entity_path(&self) -> &str {
self.snapshot.entity_path()
}
#[must_use]
pub(in crate::db) const fn entity_name(&self) -> &str {
self.snapshot.entity_name()
}
#[must_use]
fn primary_key_field(&self) -> Option<&PersistedFieldSnapshot> {
let primary_key_field_id = self.snapshot.primary_key_field_id();
self.snapshot
.fields()
.iter()
.find(|field| field.id() == primary_key_field_id)
}
#[must_use]
pub(in crate::db) fn primary_key_field_kind(&self) -> Option<&PersistedFieldKind> {
self.primary_key_field().map(PersistedFieldSnapshot::kind)
}
#[must_use]
pub(in crate::db) fn primary_key_field_name(&self) -> Option<&str> {
self.primary_key_field().map(PersistedFieldSnapshot::name)
}
#[must_use]
fn field_by_name(&self, name: &str) -> Option<&PersistedFieldSnapshot> {
self.snapshot
.fields()
.iter()
.find(|field| field.name() == name)
}
#[must_use]
pub(in crate::db) fn field_kind_by_name(&self, name: &str) -> Option<&PersistedFieldKind> {
self.field_by_name(name).map(PersistedFieldSnapshot::kind)
}
#[must_use]
pub(in crate::db) fn field_facts_by_name(
&self,
name: &str,
) -> Option<(
&PersistedFieldKind,
SchemaFieldSlot,
&[PersistedNestedLeafSnapshot],
)> {
let field = self.field_by_name(name)?;
let slot = self.snapshot.row_layout().slot_for_field(field.id())?;
Some((field.kind(), slot, field.nested_leaves()))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct PersistedSchemaSnapshot {
version: SchemaVersion,
entity_path: String,
entity_name: String,
primary_key_field_id: FieldId,
row_layout: SchemaRowLayout,
fields: Vec<PersistedFieldSnapshot>,
}
impl PersistedSchemaSnapshot {
#[must_use]
pub(in crate::db) const fn new(
version: SchemaVersion,
entity_path: String,
entity_name: String,
primary_key_field_id: FieldId,
row_layout: SchemaRowLayout,
fields: Vec<PersistedFieldSnapshot>,
) -> Self {
Self {
version,
entity_path,
entity_name,
primary_key_field_id,
row_layout,
fields,
}
}
#[must_use]
pub(in crate::db) const fn version(&self) -> SchemaVersion {
self.version
}
#[must_use]
pub(in crate::db) const fn entity_path(&self) -> &str {
self.entity_path.as_str()
}
#[must_use]
pub(in crate::db) const fn entity_name(&self) -> &str {
self.entity_name.as_str()
}
#[must_use]
pub(in crate::db) const fn primary_key_field_id(&self) -> FieldId {
self.primary_key_field_id
}
#[must_use]
pub(in crate::db) const fn row_layout(&self) -> &SchemaRowLayout {
&self.row_layout
}
#[must_use]
pub(in crate::db) const fn fields(&self) -> &[PersistedFieldSnapshot] {
self.fields.as_slice()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct PersistedFieldSnapshot {
id: FieldId,
name: String,
slot: SchemaFieldSlot,
kind: PersistedFieldKind,
nested_leaves: Vec<PersistedNestedLeafSnapshot>,
nullable: bool,
default: SchemaFieldDefault,
storage_decode: FieldStorageDecode,
leaf_codec: LeafCodec,
}
impl PersistedFieldSnapshot {
#[expect(
clippy::too_many_arguments,
reason = "schema snapshot construction keeps every persisted field contract explicit"
)]
#[must_use]
pub(in crate::db) const fn new(
id: FieldId,
name: String,
slot: SchemaFieldSlot,
kind: PersistedFieldKind,
nested_leaves: Vec<PersistedNestedLeafSnapshot>,
nullable: bool,
default: SchemaFieldDefault,
storage_decode: FieldStorageDecode,
leaf_codec: LeafCodec,
) -> Self {
Self {
id,
name,
slot,
kind,
nested_leaves,
nullable,
default,
storage_decode,
leaf_codec,
}
}
#[must_use]
pub(in crate::db) const fn id(&self) -> FieldId {
self.id
}
#[must_use]
pub(in crate::db) const fn name(&self) -> &str {
self.name.as_str()
}
#[must_use]
pub(in crate::db) const fn slot(&self) -> SchemaFieldSlot {
self.slot
}
#[must_use]
pub(in crate::db) const fn kind(&self) -> &PersistedFieldKind {
&self.kind
}
#[must_use]
pub(in crate::db) const fn nested_leaves(&self) -> &[PersistedNestedLeafSnapshot] {
self.nested_leaves.as_slice()
}
#[must_use]
pub(in crate::db) const fn nullable(&self) -> bool {
self.nullable
}
#[must_use]
pub(in crate::db) const fn default(&self) -> SchemaFieldDefault {
self.default
}
#[must_use]
pub(in crate::db) const fn storage_decode(&self) -> FieldStorageDecode {
self.storage_decode
}
#[must_use]
pub(in crate::db) const fn leaf_codec(&self) -> LeafCodec {
self.leaf_codec
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct PersistedNestedLeafSnapshot {
path: Vec<String>,
kind: PersistedFieldKind,
nullable: bool,
storage_decode: FieldStorageDecode,
leaf_codec: LeafCodec,
}
impl PersistedNestedLeafSnapshot {
#[must_use]
pub(in crate::db) const fn new(
path: Vec<String>,
kind: PersistedFieldKind,
nullable: bool,
storage_decode: FieldStorageDecode,
leaf_codec: LeafCodec,
) -> Self {
Self {
path,
kind,
nullable,
storage_decode,
leaf_codec,
}
}
#[must_use]
pub(in crate::db) const fn path(&self) -> &[String] {
self.path.as_slice()
}
#[must_use]
pub(in crate::db) const fn kind(&self) -> &PersistedFieldKind {
&self.kind
}
#[must_use]
pub(in crate::db) const fn nullable(&self) -> bool {
self.nullable
}
#[must_use]
pub(in crate::db) const fn storage_decode(&self) -> FieldStorageDecode {
self.storage_decode
}
#[must_use]
pub(in crate::db) const fn leaf_codec(&self) -> LeafCodec {
self.leaf_codec
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum SchemaFieldDefault {
None,
}
impl SchemaFieldDefault {
#[must_use]
pub(in crate::db) const fn from_model_default(default: FieldDatabaseDefault) -> Self {
match default {
FieldDatabaseDefault::None => Self::None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) enum PersistedFieldKind {
Account,
Blob,
Bool,
Date,
Decimal {
scale: u32,
},
Duration,
Enum {
path: String,
variants: Vec<PersistedEnumVariant>,
},
Float32,
Float64,
Int,
Int128,
IntBig,
Principal,
Subaccount,
Text {
max_len: Option<u32>,
},
Timestamp,
Uint,
Uint128,
UintBig,
Ulid,
Unit,
Relation {
target_path: String,
target_entity_name: String,
target_entity_tag: EntityTag,
target_store_path: String,
key_kind: Box<Self>,
strength: PersistedRelationStrength,
},
List(Box<Self>),
Set(Box<Self>),
Map {
key: Box<Self>,
value: Box<Self>,
},
Structured {
queryable: bool,
},
}
impl PersistedFieldKind {
#[must_use]
pub(in crate::db) fn from_model_kind(kind: FieldKind) -> Self {
match kind {
FieldKind::Account => Self::Account,
FieldKind::Blob => Self::Blob,
FieldKind::Bool => Self::Bool,
FieldKind::Date => Self::Date,
FieldKind::Decimal { scale } => Self::Decimal { scale },
FieldKind::Duration => Self::Duration,
FieldKind::Enum { path, variants } => Self::Enum {
path: path.to_string(),
variants: variants
.iter()
.map(|variant| PersistedEnumVariant {
ident: variant.ident().to_string(),
payload_kind: variant
.payload_kind()
.map(|payload| Box::new(Self::from_model_kind(*payload))),
payload_storage_decode: variant.payload_storage_decode(),
})
.collect(),
},
FieldKind::Float32 => Self::Float32,
FieldKind::Float64 => Self::Float64,
FieldKind::Int => Self::Int,
FieldKind::Int128 => Self::Int128,
FieldKind::IntBig => Self::IntBig,
FieldKind::Principal => Self::Principal,
FieldKind::Subaccount => Self::Subaccount,
FieldKind::Text { max_len } => Self::Text { max_len },
FieldKind::Timestamp => Self::Timestamp,
FieldKind::Uint => Self::Uint,
FieldKind::Uint128 => Self::Uint128,
FieldKind::UintBig => Self::UintBig,
FieldKind::Ulid => Self::Ulid,
FieldKind::Unit => Self::Unit,
FieldKind::Relation {
target_path,
target_entity_name,
target_entity_tag,
target_store_path,
key_kind,
strength,
} => Self::Relation {
target_path: target_path.to_string(),
target_entity_name: target_entity_name.to_string(),
target_entity_tag,
target_store_path: target_store_path.to_string(),
key_kind: Box::new(Self::from_model_kind(*key_kind)),
strength: PersistedRelationStrength::from_model_strength(strength),
},
FieldKind::List(inner) => Self::List(Box::new(Self::from_model_kind(*inner))),
FieldKind::Set(inner) => Self::Set(Box::new(Self::from_model_kind(*inner))),
FieldKind::Map { key, value } => Self::Map {
key: Box::new(Self::from_model_kind(*key)),
value: Box::new(Self::from_model_kind(*value)),
},
FieldKind::Structured { queryable } => Self::Structured { queryable },
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct PersistedEnumVariant {
ident: String,
payload_kind: Option<Box<PersistedFieldKind>>,
payload_storage_decode: FieldStorageDecode,
}
impl PersistedEnumVariant {
#[must_use]
pub(in crate::db) const fn new(
ident: String,
payload_kind: Option<Box<PersistedFieldKind>>,
payload_storage_decode: FieldStorageDecode,
) -> Self {
Self {
ident,
payload_kind,
payload_storage_decode,
}
}
#[must_use]
pub(in crate::db) const fn ident(&self) -> &str {
self.ident.as_str()
}
#[must_use]
pub(in crate::db) fn payload_kind(&self) -> Option<&PersistedFieldKind> {
match self.payload_kind.as_ref() {
Some(kind) => Some(kind.as_ref()),
None => None,
}
}
#[must_use]
pub(in crate::db) const fn payload_storage_decode(&self) -> FieldStorageDecode {
self.payload_storage_decode
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum PersistedRelationStrength {
Strong,
Weak,
}
impl PersistedRelationStrength {
#[must_use]
const fn from_model_strength(strength: RelationStrength) -> Self {
match strength {
RelationStrength::Strong => Self::Strong,
RelationStrength::Weak => Self::Weak,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::field::ScalarCodec;
fn accepted_schema_fixture() -> AcceptedSchemaSnapshot {
accepted_schema_fixture_with_payload_slots(SchemaFieldSlot::new(7), SchemaFieldSlot::new(7))
}
fn accepted_schema_fixture_with_payload_slots(
layout_slot: SchemaFieldSlot,
field_slot: SchemaFieldSlot,
) -> AcceptedSchemaSnapshot {
AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
SchemaVersion::initial(),
"schema::snapshot::tests::Asset".to_string(),
"Asset".to_string(),
FieldId::new(1),
SchemaRowLayout::new(
SchemaVersion::initial(),
vec![
(FieldId::new(1), SchemaFieldSlot::new(0)),
(FieldId::new(2), layout_slot),
],
),
vec![
PersistedFieldSnapshot::new(
FieldId::new(1),
"id".to_string(),
SchemaFieldSlot::new(0),
PersistedFieldKind::Ulid,
Vec::new(),
false,
SchemaFieldDefault::None,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Ulid),
),
PersistedFieldSnapshot::new(
FieldId::new(2),
"payload".to_string(),
field_slot,
PersistedFieldKind::Blob,
vec![PersistedNestedLeafSnapshot::new(
vec!["thumbnail".to_string()],
PersistedFieldKind::Blob,
false,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Blob),
)],
false,
SchemaFieldDefault::None,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Blob),
),
],
))
}
#[test]
fn accepted_schema_snapshot_exposes_schema_facts_without_raw_payload_access() {
let snapshot = accepted_schema_fixture();
assert_eq!(snapshot.entity_path(), "schema::snapshot::tests::Asset");
assert_eq!(snapshot.entity_name(), "Asset");
assert_eq!(snapshot.primary_key_field_name(), Some("id"));
assert_eq!(
snapshot.primary_key_field_kind(),
Some(&PersistedFieldKind::Ulid),
);
assert_eq!(
snapshot.field_kind_by_name("payload"),
Some(&PersistedFieldKind::Blob),
);
let (_, slot, nested) = snapshot
.field_facts_by_name("payload")
.expect("accepted payload facts should resolve");
assert_eq!(slot, SchemaFieldSlot::new(7));
assert_eq!(nested.len(), 1);
assert_eq!(nested[0].path(), &["thumbnail".to_string()]);
assert_eq!(snapshot.field_kind_by_name("missing"), None);
assert_eq!(snapshot.field_facts_by_name("missing"), None);
}
#[test]
fn accepted_schema_snapshot_slot_lookup_uses_row_layout_authority() {
let snapshot = accepted_schema_fixture_with_payload_slots(
SchemaFieldSlot::new(9),
SchemaFieldSlot::new(7),
);
let (_, slot, _) = snapshot
.field_facts_by_name("payload")
.expect("accepted payload facts should resolve");
assert_eq!(slot, SchemaFieldSlot::new(9));
}
#[test]
fn accepted_schema_snapshot_field_facts_use_row_layout_slot_authority() {
let snapshot = accepted_schema_fixture_with_payload_slots(
SchemaFieldSlot::new(11),
SchemaFieldSlot::new(7),
);
let (kind, slot, nested) = snapshot
.field_facts_by_name("payload")
.expect("accepted field facts should resolve");
assert_eq!(kind, &PersistedFieldKind::Blob);
assert_eq!(slot, SchemaFieldSlot::new(11));
assert_eq!(nested.len(), 1);
assert_eq!(nested[0].path(), &["thumbnail".to_string()]);
}
#[test]
fn accepted_schema_snapshot_try_new_rejects_invalid_metadata() {
let snapshot = PersistedSchemaSnapshot::new(
SchemaVersion::initial(),
"schema::snapshot::tests::Invalid".to_string(),
"Invalid".to_string(),
FieldId::new(99),
SchemaRowLayout::new(
SchemaVersion::initial(),
vec![(FieldId::new(1), SchemaFieldSlot::new(0))],
),
vec![PersistedFieldSnapshot::new(
FieldId::new(1),
"id".to_string(),
SchemaFieldSlot::new(0),
PersistedFieldKind::Ulid,
Vec::new(),
false,
SchemaFieldDefault::None,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Ulid),
)],
);
let err = AcceptedSchemaSnapshot::try_new(snapshot)
.expect_err("accepted schema construction should reject invalid metadata");
assert!(
err.message()
.contains("accepted schema snapshot primary key field missing from row layout"),
"accepted schema construction should report the integrity failure"
);
}
}