radixdb-catalog 1.1.0

Generational logical catalog owner for RadixDB
Documentation
use std::collections::{BTreeMap, BTreeSet};

use crate::{
    CatalogError, CatalogGeneration, CatalogGraph, CatalogMutation, CatalogMutationSet,
    CatalogObject, CatalogResult, ObjectId, ObjectPrecondition,
};

pub(super) fn apply_mutation_set(
    mutation_set: &CatalogMutationSet,
    current: &CatalogGeneration,
) -> CatalogResult<CatalogGraph> {
    validate_generation_precondition(mutation_set, current)?;
    for mutation in mutation_set.mutations() {
        validate_object_precondition(mutation, current)?;
    }

    let mut objects = current
        .graph()
        .objects()
        .cloned()
        .map(|object| (object.id(), object))
        .collect::<BTreeMap<_, _>>();
    let mut edges = current
        .graph()
        .edges()
        .iter()
        .copied()
        .collect::<BTreeSet<_>>();
    let mut dropped = BTreeSet::new();

    for mutation in mutation_set.mutations() {
        match mutation {
            CatalogMutation::Create { object } => {
                if object.definition_revision() != 1 {
                    return precondition_failure(
                        object.id(),
                        "new object definition revision is not one",
                    );
                }
                if objects.insert(object.id(), object.clone()).is_some() {
                    return precondition_failure(object.id(), "object already exists");
                }
            }
            CatalogMutation::Alter {
                expected,
                replacement,
            } => {
                let current_object = current_object(current, *expected)?;
                validate_alter_replacement(current_object, *expected, replacement)?;
                objects.insert(replacement.id(), replacement.clone());
            }
            CatalogMutation::Drop { expected } => {
                objects.remove(&expected.object_id());
                dropped.insert(expected.object_id());
            }
            CatalogMutation::Rename { expected, new_name } => {
                let current_object = current_object(current, *expected)?;
                if current_object.name() == new_name {
                    return precondition_failure(
                        current_object.id(),
                        "rename does not change the catalog name",
                    );
                }
                let revision = current_object.definition_revision().checked_add(1).ok_or(
                    CatalogError::InvalidCatalogMutation {
                        detail: "object definition revision overflow",
                    },
                )?;
                let renamed = CatalogObject::new(
                    current_object.id(),
                    current_object.namespace_id(),
                    current_object.parent_id(),
                    current_object.owner_principal_id(),
                    new_name.clone(),
                    revision,
                    current_object.payload().clone(),
                )?;
                objects.insert(renamed.id(), renamed);
            }
        }
    }

    for edge in mutation_set.edge_removals() {
        if !edges.remove(edge) {
            return Err(CatalogError::InvalidCatalogMutation {
                detail: "edge removal precondition is absent",
            });
        }
    }
    edges.retain(|edge| {
        !dropped.contains(&edge.source_object_id()) && !dropped.contains(&edge.target_object_id())
    });
    for edge in mutation_set.edge_additions() {
        if !edges.insert(*edge) {
            return Err(CatalogError::InvalidCatalogMutation {
                detail: "edge addition already exists",
            });
        }
    }

    let graph = CatalogGraph::build(objects.into_values().collect(), edges.into_iter().collect())?;
    let required_minor = current.format_minor().max(graph.required_format_minor());
    if mutation_set.format_minor() != required_minor {
        return Err(CatalogError::InvalidCatalogMutation {
            detail:
                "mutation-set format minor would upgrade or downgrade outside its declared envelope",
        });
    }
    Ok(graph)
}

fn validate_generation_precondition(
    mutation_set: &CatalogMutationSet,
    current: &CatalogGeneration,
) -> CatalogResult<()> {
    let meta = current.meta();
    require_expected(
        "database identity",
        hex(mutation_set.expected_database_id()),
        hex(&meta.database_id()),
    )?;
    require_expected(
        "catalog identity",
        hex(mutation_set.expected_catalog_id()),
        hex(&meta.catalog_id()),
    )?;
    require_expected(
        "catalog generation",
        mutation_set.expected_catalog_generation().to_string(),
        meta.catalog_generation().to_string(),
    )
}

fn validate_object_precondition(
    mutation: &CatalogMutation,
    current: &CatalogGeneration,
) -> CatalogResult<()> {
    match mutation {
        CatalogMutation::Create { object } => {
            if current.object(object.id()).is_some() {
                return precondition_failure(object.id(), "object already exists");
            }
            Ok(())
        }
        CatalogMutation::Alter { expected, .. }
        | CatalogMutation::Drop { expected }
        | CatalogMutation::Rename { expected, .. } => {
            current_object(current, *expected).map(|_| ())
        }
    }
}

fn current_object(
    current: &CatalogGeneration,
    expected: ObjectPrecondition,
) -> CatalogResult<&CatalogObject> {
    let object = current.object(expected.object_id()).ok_or_else(|| {
        CatalogError::CatalogObjectPreconditionFailed {
            id: expected.object_id().to_string(),
            detail: "object is absent",
        }
    })?;
    if object.kind() != expected.expected_kind() {
        return precondition_failure(object.id(), "object kind differs");
    }
    if object.definition_revision() != expected.expected_definition_revision() {
        return precondition_failure(object.id(), "definition revision differs");
    }
    Ok(object)
}

fn validate_alter_replacement(
    current: &CatalogObject,
    expected: ObjectPrecondition,
    replacement: &CatalogObject,
) -> CatalogResult<()> {
    if replacement.id() != current.id() {
        return precondition_failure(expected.object_id(), "ALTER replacement changes object ID");
    }
    if replacement.kind() != current.kind() {
        return precondition_failure(
            expected.object_id(),
            "ALTER replacement changes object kind",
        );
    }
    if replacement.namespace_id() != current.namespace_id() {
        return precondition_failure(expected.object_id(), "ALTER changes namespace");
    }
    if replacement.parent_id() != current.parent_id() {
        return precondition_failure(expected.object_id(), "ALTER changes containment parent");
    }
    let next_revision = expected
        .expected_definition_revision()
        .checked_add(1)
        .ok_or(CatalogError::InvalidCatalogMutation {
            detail: "object definition revision overflow",
        })?;
    if replacement.definition_revision() != next_revision {
        return precondition_failure(
            expected.object_id(),
            "ALTER replacement revision is not expected revision plus one",
        );
    }
    Ok(())
}

fn require_expected(field: &'static str, expected: String, actual: String) -> CatalogResult<()> {
    if expected != actual {
        return Err(CatalogError::StaleCatalogMutation {
            field,
            expected,
            actual,
        });
    }
    Ok(())
}

fn precondition_failure<T>(id: ObjectId, detail: &'static str) -> CatalogResult<T> {
    Err(CatalogError::CatalogObjectPreconditionFailed {
        id: id.to_string(),
        detail,
    })
}

fn hex(bytes: &[u8; 16]) -> String {
    let mut output = String::with_capacity(32);
    for byte in bytes {
        use std::fmt::Write;
        write!(output, "{byte:02x}").expect("writing to String cannot fail");
    }
    output
}