use crate::{
db::schema::{
FieldId, PersistedFieldKind, PersistedFieldSnapshot, PersistedSchemaSnapshot,
SchemaFieldDefault, SchemaFieldSlot, SchemaRowLayout, SchemaVersion, sql_capabilities,
},
model::{
entity::EntityModel,
field::{FieldDatabaseDefault, FieldKind, FieldModel, FieldStorageDecode, LeafCodec},
},
};
#[derive(Clone, Debug)]
pub(in crate::db) struct CompiledSchemaProposal {
entity_path: &'static str,
entity_name: &'static str,
primary_key_name: &'static str,
primary_key_field_id: FieldId,
fields: Vec<CompiledFieldProposal>,
}
impl CompiledSchemaProposal {
#[must_use]
pub(in crate::db) const fn entity_path(&self) -> &'static str {
self.entity_path
}
#[must_use]
pub(in crate::db) const fn entity_name(&self) -> &'static str {
self.entity_name
}
#[must_use]
pub(in crate::db) const fn primary_key_name(&self) -> &'static str {
self.primary_key_name
}
#[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 fields(&self) -> &[CompiledFieldProposal] {
self.fields.as_slice()
}
#[must_use]
pub(in crate::db) fn initial_row_layout(&self) -> SchemaRowLayout {
let field_to_slot = self
.fields()
.iter()
.map(|field| (field.id(), field.slot()))
.collect::<Vec<_>>();
SchemaRowLayout::new(SchemaVersion::initial(), field_to_slot)
}
#[must_use]
pub(in crate::db) fn initial_persisted_schema_snapshot(&self) -> PersistedSchemaSnapshot {
let fields = self
.fields()
.iter()
.map(CompiledFieldProposal::initial_persisted_field_snapshot)
.collect::<Vec<_>>();
PersistedSchemaSnapshot::new(
SchemaVersion::initial(),
self.entity_path().to_string(),
self.entity_name().to_string(),
self.primary_key_field_id(),
self.initial_row_layout(),
fields,
)
}
}
#[derive(Clone, Debug)]
pub(in crate::db) struct CompiledFieldProposal {
id: FieldId,
name: &'static str,
slot: SchemaFieldSlot,
kind: FieldKind,
nullable: bool,
database_default: FieldDatabaseDefault,
storage_decode: FieldStorageDecode,
leaf_codec: LeafCodec,
}
impl CompiledFieldProposal {
#[must_use]
pub(in crate::db) const fn id(&self) -> FieldId {
self.id
}
#[must_use]
pub(in crate::db) const fn name(&self) -> &'static 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) -> FieldKind {
self.kind
}
#[must_use]
pub(in crate::db) const fn nullable(&self) -> bool {
self.nullable
}
#[must_use]
pub(in crate::db) const fn database_default(&self) -> FieldDatabaseDefault {
self.database_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) fn initial_persisted_field_snapshot(&self) -> PersistedFieldSnapshot {
PersistedFieldSnapshot::new(
self.id(),
self.name().to_string(),
self.slot(),
PersistedFieldKind::from_model_kind(self.kind()),
self.nullable(),
SchemaFieldDefault::from_model_default(self.database_default()),
self.storage_decode(),
self.leaf_codec(),
)
}
}
#[must_use]
pub(in crate::db) fn compiled_schema_proposal_for_model(
model: &EntityModel,
) -> CompiledSchemaProposal {
let fields = model
.fields()
.iter()
.enumerate()
.map(compiled_field_proposal_from_model_field)
.collect::<Vec<_>>();
let proposal = CompiledSchemaProposal {
entity_path: model.path(),
entity_name: model.name(),
primary_key_name: model.primary_key().name(),
primary_key_field_id: FieldId::from_initial_slot(model.primary_key_slot()),
fields,
};
debug_assert_compiled_schema_proposal_invariants(model, &proposal);
proposal
}
fn debug_assert_compiled_schema_proposal_invariants(
model: &EntityModel,
proposal: &CompiledSchemaProposal,
) {
debug_assert_eq!(
proposal.primary_key_field_id(),
FieldId::from_initial_slot(model.primary_key_slot())
);
let layout = proposal.initial_row_layout();
let snapshot = proposal.initial_persisted_schema_snapshot();
debug_assert_eq!(layout.version(), SchemaVersion::initial());
debug_assert_eq!(layout.version().get(), SchemaVersion::initial().get());
debug_assert_eq!(layout.field_to_slot().len(), proposal.fields().len());
debug_assert_eq!(snapshot.version(), SchemaVersion::initial());
debug_assert_eq!(snapshot.entity_path(), proposal.entity_path());
debug_assert_eq!(snapshot.entity_name(), proposal.entity_name());
debug_assert_eq!(
snapshot.primary_key_field_id(),
proposal.primary_key_field_id()
);
debug_assert_eq!(snapshot.row_layout(), &layout);
debug_assert_eq!(snapshot.fields().len(), proposal.fields().len());
for field in snapshot.fields() {
let _ = (
field.id(),
field.name(),
field.slot(),
field.kind(),
field.nullable(),
field.default(),
field.storage_decode(),
field.leaf_codec(),
);
let capabilities = sql_capabilities(field.kind());
let aggregate = capabilities.aggregate_input();
let _ = (
capabilities.selectable(),
capabilities.comparable(),
capabilities.orderable(),
capabilities.groupable(),
aggregate.count(),
aggregate.numeric(),
aggregate.extrema(),
);
}
for (expected_slot, field) in proposal.fields().iter().enumerate() {
debug_assert_eq!(field.id(), FieldId::from_initial_slot(expected_slot));
debug_assert_eq!(
field.slot(),
SchemaFieldSlot::from_generated_index(expected_slot)
);
let _ = (
field.name(),
field.kind(),
field.nullable(),
field.database_default(),
field.storage_decode(),
field.leaf_codec(),
field.initial_persisted_field_snapshot(),
);
}
}
fn compiled_field_proposal_from_model_field(
(slot, field): (usize, &FieldModel),
) -> CompiledFieldProposal {
let slot = SchemaFieldSlot::from_generated_index(slot);
CompiledFieldProposal {
id: FieldId::from_initial_slot(usize::from(slot.get())),
name: field.name(),
slot,
kind: field.kind(),
nullable: field.nullable(),
database_default: field.database_default(),
storage_decode: field.storage_decode(),
leaf_codec: field.leaf_codec(),
}
}
#[cfg(test)]
mod tests {
use crate::{
db::schema::{
FieldId, PersistedFieldKind, SchemaFieldDefault, SchemaFieldSlot, SchemaVersion,
compiled_schema_proposal_for_model,
},
model::{
entity::EntityModel,
field::{
FieldDatabaseDefault, FieldKind, FieldModel, FieldStorageDecode, LeafCodec,
ScalarCodec,
},
index::IndexModel,
},
testing::entity_model_from_static,
};
static FIELDS: [FieldModel; 3] = [
FieldModel::generated("id", FieldKind::Ulid),
FieldModel::generated_with_storage_decode_and_nullability(
"name",
FieldKind::Text { max_len: None },
FieldStorageDecode::ByKind,
true,
),
FieldModel::generated("rank", FieldKind::Uint),
];
static INDEXES: [&IndexModel; 0] = [];
static MODEL: EntityModel = entity_model_from_static(
"schema::proposal::tests::Entity",
"Entity",
&FIELDS[0],
0,
&FIELDS,
&INDEXES,
);
#[test]
fn compiled_schema_proposal_assigns_initial_field_ids_from_slots() {
let proposal = compiled_schema_proposal_for_model(&MODEL);
assert_eq!(proposal.entity_path(), "schema::proposal::tests::Entity");
assert_eq!(proposal.entity_name(), "Entity");
assert_eq!(proposal.primary_key_field_id(), FieldId::new(1));
assert_eq!(proposal.fields().len(), 3);
let ids = proposal
.fields()
.iter()
.map(super::CompiledFieldProposal::id)
.collect::<Vec<_>>();
assert_eq!(ids, vec![FieldId::new(1), FieldId::new(2), FieldId::new(3)]);
}
#[test]
fn compiled_schema_proposal_preserves_generated_storage_contracts() {
let proposal = compiled_schema_proposal_for_model(&MODEL);
let name = &proposal.fields()[1];
assert_eq!(name.name(), "name");
assert_eq!(name.slot(), SchemaFieldSlot::from_generated_index(1));
assert!(matches!(name.kind(), FieldKind::Text { max_len: None }));
assert!(name.nullable());
assert_eq!(name.database_default(), FieldDatabaseDefault::None);
assert_eq!(name.storage_decode(), FieldStorageDecode::ByKind);
assert_eq!(name.leaf_codec(), LeafCodec::Scalar(ScalarCodec::Text));
}
#[test]
fn compiled_schema_proposal_builds_initial_row_layout() {
let proposal = compiled_schema_proposal_for_model(&MODEL);
let layout = proposal.initial_row_layout();
assert_eq!(layout.version(), SchemaVersion::initial());
assert_eq!(
layout.field_to_slot(),
&[
(FieldId::new(1), SchemaFieldSlot::from_generated_index(0)),
(FieldId::new(2), SchemaFieldSlot::from_generated_index(1)),
(FieldId::new(3), SchemaFieldSlot::from_generated_index(2)),
]
);
}
#[test]
fn compiled_schema_proposal_builds_initial_persisted_snapshot() {
let proposal = compiled_schema_proposal_for_model(&MODEL);
let snapshot = proposal.initial_persisted_schema_snapshot();
assert_eq!(snapshot.version(), SchemaVersion::initial());
assert_eq!(snapshot.entity_path(), "schema::proposal::tests::Entity");
assert_eq!(snapshot.entity_name(), "Entity");
assert_eq!(snapshot.primary_key_field_id(), FieldId::new(1));
assert_eq!(snapshot.fields().len(), 3);
let name = &snapshot.fields()[1];
assert_eq!(name.id(), FieldId::new(2));
assert_eq!(name.name(), "name");
assert_eq!(name.slot(), SchemaFieldSlot::from_generated_index(1));
assert!(matches!(
name.kind(),
PersistedFieldKind::Text { max_len: None }
));
assert!(name.nullable());
assert_eq!(name.default(), SchemaFieldDefault::None);
assert_eq!(name.storage_decode(), FieldStorageDecode::ByKind);
assert_eq!(name.leaf_codec(), LeafCodec::Scalar(ScalarCodec::Text));
}
}