use super::SchemaMutationRequest;
use crate::db::schema::{PersistedFieldSnapshot, PersistedIndexSnapshot, PersistedSchemaSnapshot};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db::schema) enum SchemaMutationDelta<'a> {
AppendOnlyFields(&'a [PersistedFieldSnapshot]),
AddFieldPathIndex(&'a PersistedIndexSnapshot),
AddExpressionIndex(&'a PersistedIndexSnapshot),
ExactMatch,
Incompatible,
}
pub(in crate::db::schema) fn classify_schema_mutation_delta<'a>(
actual: &PersistedSchemaSnapshot,
expected: &'a PersistedSchemaSnapshot,
) -> SchemaMutationDelta<'a> {
if actual == expected {
return SchemaMutationDelta::ExactMatch;
}
if let Some(fields) = append_only_additive_fields(actual, expected) {
return SchemaMutationDelta::AppendOnlyFields(fields);
}
if let Some(index) = single_added_index(actual, expected)
&& SchemaMutationRequest::from_accepted_field_path_index(index).is_ok()
{
return SchemaMutationDelta::AddFieldPathIndex(index);
}
if let Some(index) = single_added_index(actual, expected)
&& SchemaMutationRequest::from_accepted_expression_index(index).is_ok()
{
return SchemaMutationDelta::AddExpressionIndex(index);
}
SchemaMutationDelta::Incompatible
}
pub(in crate::db::schema) fn schema_mutation_request_for_snapshots<'a>(
actual: &PersistedSchemaSnapshot,
expected: &'a PersistedSchemaSnapshot,
) -> Option<SchemaMutationRequest<'a>> {
match classify_schema_mutation_delta(actual, expected) {
SchemaMutationDelta::AppendOnlyFields(fields) => {
Some(SchemaMutationRequest::AppendOnlyFields(fields))
}
SchemaMutationDelta::AddFieldPathIndex(index) => {
SchemaMutationRequest::from_accepted_field_path_index(index).ok()
}
SchemaMutationDelta::AddExpressionIndex(index) => {
SchemaMutationRequest::from_accepted_expression_index(index).ok()
}
SchemaMutationDelta::ExactMatch => Some(SchemaMutationRequest::ExactMatch),
SchemaMutationDelta::Incompatible => None,
}
}
fn append_only_additive_fields<'a>(
actual: &PersistedSchemaSnapshot,
expected: &'a PersistedSchemaSnapshot,
) -> Option<&'a [PersistedFieldSnapshot]> {
if actual.row_layout().history_floor() != expected.row_layout().history_floor() {
return None;
}
append_only_additive_fields_with_admitted_floor(actual, expected)
}
#[cfg(feature = "sql")]
pub(in crate::db::schema) fn required_empty_entity_field_addition_matches(
actual: &PersistedSchemaSnapshot,
expected: &PersistedSchemaSnapshot,
field: &PersistedFieldSnapshot,
) -> bool {
if !matches!(
field.historical_fill(),
crate::db::schema::SchemaHistoricalFill::Reject
) || expected.row_layout().history_floor() != expected.row_layout().current_version()
{
return false;
}
append_only_additive_fields_with_admitted_floor(actual, expected)
.is_some_and(|added| added == std::slice::from_ref(field))
}
fn append_only_additive_fields_with_admitted_floor<'a>(
actual: &PersistedSchemaSnapshot,
expected: &'a PersistedSchemaSnapshot,
) -> Option<&'a [PersistedFieldSnapshot]> {
let next_layout_version = actual.row_layout().current_version().checked_next()?;
if actual.fields().len() >= expected.fields().len()
|| actual.row_layout().field_to_slot().len() >= expected.row_layout().field_to_slot().len()
|| expected.row_layout().current_version() != next_layout_version
|| actual.entity_path() != expected.entity_path()
|| actual.entity_name() != expected.entity_name()
|| actual.primary_key_field_ids() != expected.primary_key_field_ids()
|| actual.indexes() != expected.indexes()
|| actual.relations() != expected.relations()
{
return None;
}
if !actual
.fields()
.iter()
.zip(expected.fields())
.all(|(actual_field, expected_field)| actual_field == expected_field)
{
return None;
}
if !actual
.row_layout()
.field_to_slot()
.iter()
.zip(expected.row_layout().field_to_slot())
.all(|(actual_pair, expected_pair)| actual_pair == expected_pair)
{
return None;
}
let added = &expected.fields()[actual.fields().len()..];
let expected_catalog = added
.iter()
.try_fold(actual.constraint_catalog().clone(), |catalog, field| {
catalog.with_added_not_null(field)
})
.ok()?;
if &expected_catalog != expected.constraint_catalog() {
return None;
}
Some(added)
}
fn single_added_index<'a>(
actual: &PersistedSchemaSnapshot,
expected: &'a PersistedSchemaSnapshot,
) -> Option<&'a PersistedIndexSnapshot> {
if actual.entity_path() != expected.entity_path()
|| actual.entity_name() != expected.entity_name()
|| actual.primary_key_field_ids() != expected.primary_key_field_ids()
|| actual.row_layout() != expected.row_layout()
|| actual.fields() != expected.fields()
|| actual.relations() != expected.relations()
|| expected.indexes().len() != actual.indexes().len().saturating_add(1)
{
return None;
}
if !actual
.indexes()
.iter()
.zip(expected.indexes())
.all(|(actual_index, expected_index)| actual_index == expected_index)
{
return None;
}
let added = expected.indexes().last()?;
let expected_catalog = actual
.constraint_catalog()
.clone()
.with_added_unique(added)
.ok()?;
(&expected_catalog == expected.constraint_catalog()).then_some(added)
}