use crate::{
db::schema::{
AcceptedSchemaSnapshot, FieldId, PersistedFieldKind, PersistedNestedLeafSnapshot,
SchemaFieldDefault, SchemaFieldSlot, SchemaVersion,
},
error::InternalError,
model::{
entity::EntityModel,
field::{FieldStorageDecode, LeafCodec},
},
};
#[allow(
dead_code,
reason = "0.147 introduces the accepted layout runtime boundary before row decode consumes it"
)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum AcceptedFieldAbsencePolicy {
NullIfMissing,
Required,
}
#[allow(
dead_code,
reason = "0.147 introduces the accepted layout runtime boundary before row decode consumes it"
)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct AcceptedRowLayoutRuntimeField<'a> {
field_id: FieldId,
name: &'a str,
slot: SchemaFieldSlot,
kind: &'a PersistedFieldKind,
nested_leaves: &'a [PersistedNestedLeafSnapshot],
nullable: bool,
default: SchemaFieldDefault,
storage_decode: FieldStorageDecode,
leaf_codec: LeafCodec,
absence_policy: AcceptedFieldAbsencePolicy,
}
#[allow(
dead_code,
reason = "0.147 introduces the accepted layout runtime boundary before row decode consumes it"
)]
impl<'a> AcceptedRowLayoutRuntimeField<'a> {
#[must_use]
pub(in crate::db) const fn field_id(&self) -> FieldId {
self.field_id
}
#[must_use]
pub(in crate::db) const fn name(&self) -> &'a str {
self.name
}
#[must_use]
pub(in crate::db) const fn slot(&self) -> SchemaFieldSlot {
self.slot
}
#[must_use]
pub(in crate::db) const fn kind(&self) -> &'a PersistedFieldKind {
self.kind
}
#[must_use]
pub(in crate::db) const fn nested_leaves(&self) -> &'a [PersistedNestedLeafSnapshot] {
self.nested_leaves
}
#[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
}
#[must_use]
pub(in crate::db) const fn absence_policy(&self) -> AcceptedFieldAbsencePolicy {
self.absence_policy
}
}
#[allow(
dead_code,
reason = "0.147 introduces the accepted layout runtime boundary before row decode consumes it"
)]
#[derive(Debug, Eq, PartialEq)]
pub(in crate::db) struct AcceptedRowLayoutRuntimeDescriptor<'a> {
version: SchemaVersion,
required_slot_count: usize,
primary_key_name: &'a str,
primary_key_slot_index: usize,
fields: Vec<AcceptedRowLayoutRuntimeField<'a>>,
}
#[allow(
dead_code,
reason = "0.147 introduces the accepted layout runtime boundary before row decode consumes it"
)]
impl<'a> AcceptedRowLayoutRuntimeDescriptor<'a> {
pub(in crate::db) fn from_accepted_schema(
accepted: &'a AcceptedSchemaSnapshot,
) -> Result<Self, InternalError> {
let snapshot = accepted.persisted_snapshot();
let row_layout = snapshot.row_layout();
let mut required_slot_count = 0usize;
let mut fields = Vec::with_capacity(snapshot.fields().len());
for field in snapshot.fields() {
let Some(slot) = row_layout.slot_for_field(field.id()) else {
return Err(InternalError::store_invariant(format!(
"accepted row layout runtime descriptor missing slot for field_id={}",
field.id().get(),
)));
};
let slot_end = usize::from(slot.get()).saturating_add(1);
required_slot_count = required_slot_count.max(slot_end);
fields.push(AcceptedRowLayoutRuntimeField {
field_id: field.id(),
name: field.name(),
slot,
kind: field.kind(),
nested_leaves: field.nested_leaves(),
nullable: field.nullable(),
default: field.default(),
storage_decode: field.storage_decode(),
leaf_codec: field.leaf_codec(),
absence_policy: accepted_field_absence_policy(field.nullable(), field.default()),
});
}
let Some(primary_key_field) = fields
.iter()
.find(|field| field.field_id() == snapshot.primary_key_field_id())
else {
return Err(InternalError::store_invariant(format!(
"accepted row layout runtime descriptor missing primary-key field_id={}",
snapshot.primary_key_field_id().get(),
)));
};
let primary_key_name = primary_key_field.name();
let primary_key_slot_index = usize::from(primary_key_field.slot().get());
Ok(Self {
version: row_layout.version(),
required_slot_count,
primary_key_name,
primary_key_slot_index,
fields,
})
}
#[must_use]
pub(in crate::db) const fn version(&self) -> SchemaVersion {
self.version
}
#[must_use]
pub(in crate::db) const fn required_slot_count(&self) -> usize {
self.required_slot_count
}
#[must_use]
pub(in crate::db) const fn primary_key_name(&self) -> &'a str {
self.primary_key_name
}
#[must_use]
pub(in crate::db) const fn primary_key_slot_index(&self) -> usize {
self.primary_key_slot_index
}
#[must_use]
pub(in crate::db) const fn fields(&self) -> &[AcceptedRowLayoutRuntimeField<'a>] {
self.fields.as_slice()
}
#[must_use]
pub(in crate::db) fn field_for_slot(
&self,
slot: SchemaFieldSlot,
) -> Option<&AcceptedRowLayoutRuntimeField<'a>> {
self.fields.iter().find(|field| field.slot() == slot)
}
#[must_use]
pub(in crate::db) fn field_for_id(
&self,
field_id: FieldId,
) -> Option<&AcceptedRowLayoutRuntimeField<'a>> {
self.fields
.iter()
.find(|field| field.field_id() == field_id)
}
#[must_use]
pub(in crate::db) fn field_by_name(
&self,
name: &str,
) -> Option<&AcceptedRowLayoutRuntimeField<'a>> {
self.fields.iter().find(|field| field.name() == name)
}
#[must_use]
pub(in crate::db) fn field_slot_index_by_name(&self, name: &str) -> Option<usize> {
self.field_by_name(name)
.map(|field| usize::from(field.slot().get()))
}
#[must_use]
pub(in crate::db) fn field_kind_by_name(&self, name: &str) -> Option<&PersistedFieldKind> {
self.field_by_name(name)
.map(AcceptedRowLayoutRuntimeField::kind)
}
pub(in crate::db) fn validate_generated_compatible_model(
&self,
model: &'static EntityModel,
) -> Result<(), InternalError> {
if self.primary_key_name() != model.primary_key.name {
return Err(InternalError::store_invariant(format!(
"accepted row layout primary key is not generated-compatible: accepted_primary_key='{}' generated_primary_key='{}'",
self.primary_key_name(),
model.primary_key.name,
)));
}
if self.required_slot_count() != model.fields().len() {
return Err(InternalError::store_invariant(format!(
"accepted row layout field count is not generated-compatible: accepted={} generated={}",
self.required_slot_count(),
model.fields().len(),
)));
}
for (generated_slot, field) in model.fields().iter().enumerate() {
let Some(accepted_field) = self.field_by_name(field.name()) else {
return Err(InternalError::store_invariant(format!(
"accepted row layout missing generated field '{}'",
field.name(),
)));
};
let accepted_slot = self
.field_slot_index_by_name(field.name())
.expect("accepted field must have a descriptor-owned slot");
if accepted_slot != generated_slot {
return Err(InternalError::store_invariant(format!(
"accepted row layout slot is not generated-compatible: field='{}' accepted_slot={} generated_slot={}",
field.name(),
accepted_slot,
generated_slot,
)));
}
let generated_kind = PersistedFieldKind::from_model_kind(field.kind());
if accepted_field.kind() != &generated_kind {
return Err(InternalError::store_invariant(format!(
"accepted row layout kind is not generated-compatible: field='{}' accepted_kind={:?} generated_kind={:?}",
field.name(),
accepted_field.kind(),
generated_kind,
)));
}
if accepted_field.storage_decode() != field.storage_decode() {
return Err(InternalError::store_invariant(format!(
"accepted row layout storage decode is not generated-compatible: field='{}' accepted_storage_decode={:?} generated_storage_decode={:?}",
field.name(),
accepted_field.storage_decode(),
field.storage_decode(),
)));
}
if accepted_field.leaf_codec() != field.leaf_codec() {
return Err(InternalError::store_invariant(format!(
"accepted row layout leaf codec is not generated-compatible: field='{}' accepted_leaf_codec={:?} generated_leaf_codec={:?}",
field.name(),
accepted_field.leaf_codec(),
field.leaf_codec(),
)));
}
}
Ok(())
}
}
#[allow(
dead_code,
reason = "0.147 introduces the accepted layout runtime boundary before row decode consumes it"
)]
const fn accepted_field_absence_policy(
nullable: bool,
default: SchemaFieldDefault,
) -> AcceptedFieldAbsencePolicy {
match (nullable, default) {
(true, SchemaFieldDefault::None) => AcceptedFieldAbsencePolicy::NullIfMissing,
(false, SchemaFieldDefault::None) => AcceptedFieldAbsencePolicy::Required,
}
}
#[cfg(test)]
mod tests {
use crate::{
db::schema::{
AcceptedSchemaSnapshot, FieldId, PersistedFieldKind, PersistedFieldSnapshot,
PersistedSchemaSnapshot, SchemaFieldDefault, SchemaFieldSlot, SchemaRowLayout,
SchemaVersion,
runtime::{AcceptedFieldAbsencePolicy, AcceptedRowLayoutRuntimeDescriptor},
},
model::field::{FieldStorageDecode, LeafCodec, ScalarCodec},
};
fn accepted_schema_fixture() -> AcceptedSchemaSnapshot {
AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
SchemaVersion::initial(),
"schema::tests::RuntimeEntity".to_string(),
"RuntimeEntity".to_string(),
FieldId::new(1),
SchemaRowLayout::new(
SchemaVersion::initial(),
vec![
(FieldId::new(1), SchemaFieldSlot::new(0)),
(FieldId::new(2), SchemaFieldSlot::new(9)),
],
),
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),
"nickname".to_string(),
SchemaFieldSlot::new(1),
PersistedFieldKind::Text { max_len: Some(32) },
Vec::new(),
true,
SchemaFieldDefault::None,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Text),
),
],
))
}
#[test]
fn accepted_row_layout_runtime_descriptor_uses_row_layout_slot_authority() {
let accepted = accepted_schema_fixture();
let descriptor = AcceptedRowLayoutRuntimeDescriptor::from_accepted_schema(&accepted)
.expect("accepted runtime descriptor should build");
assert_eq!(descriptor.version(), SchemaVersion::initial());
assert_eq!(descriptor.required_slot_count(), 10);
assert_eq!(descriptor.primary_key_name(), "id");
assert_eq!(descriptor.primary_key_slot_index(), 0);
assert_eq!(descriptor.fields().len(), 2);
let nickname = descriptor
.fields()
.iter()
.find(|field| field.name() == "nickname")
.expect("nickname field should be present");
assert_eq!(nickname.field_id(), FieldId::new(2));
assert_eq!(nickname.slot(), SchemaFieldSlot::new(9));
assert_eq!(
nickname.absence_policy(),
AcceptedFieldAbsencePolicy::NullIfMissing
);
assert_eq!(nickname.default(), SchemaFieldDefault::None);
assert_eq!(nickname.storage_decode(), FieldStorageDecode::ByKind);
assert_eq!(nickname.leaf_codec(), LeafCodec::Scalar(ScalarCodec::Text));
assert!(matches!(
nickname.kind(),
PersistedFieldKind::Text { max_len: Some(32) },
));
assert_eq!(
descriptor
.field_for_slot(SchemaFieldSlot::new(9))
.expect("nickname should be indexed by accepted slot")
.name(),
"nickname",
);
assert_eq!(
descriptor
.field_for_id(FieldId::new(2))
.expect("nickname should be indexed by durable field ID")
.slot(),
SchemaFieldSlot::new(9),
);
assert_eq!(
descriptor
.field_by_name("nickname")
.expect("nickname should be indexed by persisted field name")
.field_id(),
FieldId::new(2),
);
assert_eq!(descriptor.field_slot_index_by_name("nickname"), Some(9));
assert!(matches!(
descriptor.field_kind_by_name("nickname"),
Some(PersistedFieldKind::Text { max_len: Some(32) }),
));
assert!(nickname.nested_leaves().is_empty());
assert!(nickname.nullable());
}
#[test]
fn accepted_row_layout_runtime_descriptor_rejects_missing_layout_slot() {
let accepted = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
SchemaVersion::initial(),
"schema::tests::BrokenEntity".to_string(),
"BrokenEntity".to_string(),
FieldId::new(1),
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),
),
PersistedFieldSnapshot::new(
FieldId::new(2),
"nickname".to_string(),
SchemaFieldSlot::new(1),
PersistedFieldKind::Text { max_len: None },
Vec::new(),
true,
SchemaFieldDefault::None,
FieldStorageDecode::ByKind,
LeafCodec::Scalar(ScalarCodec::Text),
),
],
));
let err = AcceptedRowLayoutRuntimeDescriptor::from_accepted_schema(&accepted)
.expect_err("missing row-layout slot should fail closed");
assert!(
err.to_string().contains("missing slot for field_id=2"),
"unexpected descriptor error: {err}",
);
}
}