lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

catalog.rs (35412B)


      1 //! Immutable expected SQLite schema-object catalogs.
      2 
      3 use core::fmt;
      4 use std::{collections::BTreeSet, error::Error};
      5 
      6 use sha2::{Digest, Sha256};
      7 
      8 use crate::{MigrationCatalog, MigrationChecksum};
      9 
     10 pub(crate) const MAX_SCHEMA_OBJECT_COUNT: usize = 4096;
     11 pub(crate) const MAX_SCHEMA_SQL_UTF8_BYTES: usize = 1024 * 1024;
     12 pub(crate) const MAX_SCHEMA_CATALOG_UTF8_BYTES: usize = 16 * 1024 * 1024;
     13 const MAX_SCHEMA_NAME_UTF8_BYTES: usize = 128;
     14 const MAX_SCHEMA_VERSION_COUNT: usize = 4097;
     15 
     16 const OBJECT_DOMAIN: &[u8] = b"radroots.service_sqlite.schema_object.v1\0";
     17 const SNAPSHOT_DOMAIN: &[u8] = b"radroots.service_sqlite.schema_snapshot.v1\0";
     18 const CATALOG_DOMAIN: &[u8] = b"radroots.service_sqlite.schema_catalog.v1\0";
     19 
     20 pub(crate) const CREATE_METADATA_TABLE_SQL: &str = r#"CREATE TABLE radroots_service_metadata (
     21     singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
     22     service_id TEXT NOT NULL,
     23     instance_id TEXT NOT NULL,
     24     source_generation BLOB NOT NULL CHECK (length(source_generation) = 32),
     25     state_schema_version INTEGER NOT NULL
     26         CHECK (state_schema_version BETWEEN 1 AND 4294967295),
     27     created_at_unix_ms INTEGER NOT NULL CHECK (created_at_unix_ms > 0)
     28 ) STRICT"#;
     29 pub(crate) const CREATE_METADATA_GUARD_TRIGGER_SQL: &str = r#"CREATE TRIGGER radroots_service_metadata_guard_update
     30 BEFORE UPDATE ON radroots_service_metadata
     31 WHEN NEW.singleton != OLD.singleton
     32     OR NEW.service_id != OLD.service_id
     33     OR NEW.instance_id != OLD.instance_id
     34     OR NEW.source_generation != OLD.source_generation
     35     OR NEW.created_at_unix_ms != OLD.created_at_unix_ms
     36     OR NEW.state_schema_version <= OLD.state_schema_version
     37 BEGIN
     38     SELECT RAISE(ABORT, 'service metadata identity is immutable');
     39 END"#;
     40 pub(crate) const CREATE_METADATA_NO_DELETE_TRIGGER_SQL: &str = r#"CREATE TRIGGER radroots_service_metadata_no_delete
     41 BEFORE DELETE ON radroots_service_metadata
     42 BEGIN
     43     SELECT RAISE(ABORT, 'service metadata is immutable');
     44 END"#;
     45 pub(crate) const CREATE_MIGRATION_LEDGER_TABLE_SQL: &str = r#"CREATE TABLE schema_migrations (
     46     version INTEGER NOT NULL PRIMARY KEY
     47         CHECK (version BETWEEN 2 AND 4294967295),
     48     name TEXT NOT NULL UNIQUE
     49         CHECK (length(CAST(name AS BLOB)) BETWEEN 1 AND 128),
     50     checksum BLOB NOT NULL CHECK (length(checksum) = 32),
     51     applied_at_unix_s INTEGER NOT NULL CHECK (applied_at_unix_s BETWEEN 0 AND 9223372036854775807),
     52     service_version TEXT NOT NULL CHECK (length(CAST(service_version AS BLOB)) BETWEEN 1 AND 128),
     53     service_commit TEXT NOT NULL CHECK (length(CAST(service_commit AS BLOB)) = 40),
     54     lib_revision TEXT NOT NULL CHECK (length(CAST(lib_revision AS BLOB)) = 40),
     55     rust_version TEXT NOT NULL CHECK (length(CAST(rust_version AS BLOB)) BETWEEN 1 AND 128),
     56     target TEXT NOT NULL CHECK (length(CAST(target AS BLOB)) BETWEEN 1 AND 128),
     57     feature_profile TEXT NOT NULL CHECK (length(CAST(feature_profile AS BLOB)) BETWEEN 1 AND 128),
     58     config_contract_version INTEGER NOT NULL CHECK (config_contract_version BETWEEN 1 AND 4294967295),
     59     state_contract_version INTEGER NOT NULL CHECK (state_contract_version BETWEEN 1 AND 4294967295),
     60     admin_contract_version INTEGER NOT NULL CHECK (admin_contract_version BETWEEN 1 AND 4294967295),
     61     status_contract_version INTEGER NOT NULL CHECK (status_contract_version BETWEEN 1 AND 4294967295),
     62     provider_contract_version INTEGER NOT NULL CHECK (provider_contract_version BETWEEN 1 AND 4294967295)
     63 ) STRICT"#;
     64 pub(crate) const CREATE_MIGRATION_NO_UPDATE_TRIGGER_SQL: &str = r#"CREATE TRIGGER schema_migrations_no_update
     65 BEFORE UPDATE ON schema_migrations
     66 BEGIN
     67     SELECT RAISE(ABORT, 'migration history is immutable');
     68 END"#;
     69 pub(crate) const CREATE_MIGRATION_NO_DELETE_TRIGGER_SQL: &str = r#"CREATE TRIGGER schema_migrations_no_delete
     70 BEFORE DELETE ON schema_migrations
     71 BEGIN
     72     SELECT RAISE(ABORT, 'migration history is immutable');
     73 END"#;
     74 
     75 #[cfg(any(target_os = "linux", target_os = "macos"))]
     76 pub(crate) const METADATA_SCHEMA_SQL: [&str; 3] = [
     77     CREATE_METADATA_TABLE_SQL,
     78     CREATE_METADATA_GUARD_TRIGGER_SQL,
     79     CREATE_METADATA_NO_DELETE_TRIGGER_SQL,
     80 ];
     81 #[cfg(any(target_os = "linux", target_os = "macos"))]
     82 pub(crate) const MIGRATION_LEDGER_SCHEMA_SQL: [&str; 3] = [
     83     CREATE_MIGRATION_LEDGER_TABLE_SQL,
     84     CREATE_MIGRATION_NO_UPDATE_TRIGGER_SQL,
     85     CREATE_MIGRATION_NO_DELETE_TRIGGER_SQL,
     86 ];
     87 
     88 /// Supported persistent object kinds in a governed service schema.
     89 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     90 pub enum SchemaObjectKind {
     91     Table,
     92     Index,
     93     Trigger,
     94 }
     95 
     96 impl SchemaObjectKind {
     97     pub(crate) const fn tag(self) -> u8 {
     98         match self {
     99             Self::Table => 0,
    100             Self::Index => 1,
    101             Self::Trigger => 2,
    102         }
    103     }
    104 
    105     #[cfg(any(target_os = "linux", target_os = "macos"))]
    106     pub(crate) fn from_sqlite(value: &str) -> Option<Self> {
    107         match value {
    108             "table" => Some(Self::Table),
    109             "index" => Some(Self::Index),
    110             "trigger" => Some(Self::Trigger),
    111             _ => None,
    112         }
    113     }
    114 }
    115 
    116 /// A SHA-256 digest over an object, version snapshot, or bound schema catalog.
    117 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
    118 pub struct SchemaDigest([u8; 32]);
    119 
    120 impl SchemaDigest {
    121     /// Constructs an independently reviewed digest from exact bytes.
    122     #[must_use]
    123     pub const fn from_bytes(bytes: [u8; 32]) -> Self {
    124         Self(bytes)
    125     }
    126 
    127     /// Returns the exact digest bytes.
    128     #[must_use]
    129     pub const fn as_bytes(&self) -> &[u8; 32] {
    130         &self.0
    131     }
    132 }
    133 
    134 impl fmt::Debug for SchemaDigest {
    135     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    136         formatter.write_str("SchemaDigest([redacted])")
    137     }
    138 }
    139 
    140 /// One immutable service-owned SQLite schema object definition.
    141 #[derive(Clone, PartialEq, Eq)]
    142 pub struct SchemaObject {
    143     kind: SchemaObjectKind,
    144     name: &'static str,
    145     table_name: &'static str,
    146     sql: &'static str,
    147     digest: SchemaDigest,
    148 }
    149 
    150 impl SchemaObject {
    151     /// Validates an embedded object definition and its independently pinned digest.
    152     pub fn new(
    153         kind: SchemaObjectKind,
    154         name: &'static str,
    155         table_name: &'static str,
    156         sql: &'static str,
    157         expected_digest: SchemaDigest,
    158     ) -> Result<Self, SchemaCatalogContractError> {
    159         validate_service_object(kind, name, table_name, sql)?;
    160         let actual_digest = object_digest(kind, name, table_name, sql);
    161         if actual_digest != expected_digest {
    162             return Err(SchemaCatalogContractError::ObjectDigestMismatch);
    163         }
    164         Ok(Self {
    165             kind,
    166             name,
    167             table_name,
    168             sql,
    169             digest: actual_digest,
    170         })
    171     }
    172 
    173     #[must_use]
    174     pub const fn kind(&self) -> SchemaObjectKind {
    175         self.kind
    176     }
    177 
    178     #[must_use]
    179     pub const fn name(&self) -> &'static str {
    180         self.name
    181     }
    182 
    183     #[must_use]
    184     pub const fn table_name(&self) -> &'static str {
    185         self.table_name
    186     }
    187 
    188     #[must_use]
    189     pub const fn digest(&self) -> SchemaDigest {
    190         self.digest
    191     }
    192 
    193     /// Computes the frozen object digest for independent pin generation.
    194     pub fn computed_digest(
    195         kind: SchemaObjectKind,
    196         name: &str,
    197         table_name: &str,
    198         sql: &str,
    199     ) -> Result<SchemaDigest, SchemaCatalogContractError> {
    200         validate_service_object(kind, name, table_name, sql)?;
    201         Ok(object_digest(kind, name, table_name, sql))
    202     }
    203 }
    204 
    205 impl fmt::Debug for SchemaObject {
    206     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    207         formatter
    208             .debug_struct("SchemaObject")
    209             .field("kind", &self.kind)
    210             .field("name", &self.name)
    211             .field("table_name", &self.table_name)
    212             .field("digest", &self.digest)
    213             .field("sql", &"[redacted]")
    214             .finish()
    215     }
    216 }
    217 
    218 /// Exact expected non-internal object snapshot for one schema version.
    219 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    220 pub struct SchemaVersionCatalog {
    221     version: u32,
    222     object_count: u32,
    223     digest: SchemaDigest,
    224 }
    225 
    226 impl SchemaVersionCatalog {
    227     /// Validates service-owned objects, adds the shared objects, and pins the snapshot.
    228     pub fn new<I>(
    229         version: u32,
    230         service_objects: I,
    231         expected_digest: SchemaDigest,
    232     ) -> Result<Self, SchemaCatalogContractError>
    233     where
    234         I: IntoIterator<Item = SchemaObject>,
    235     {
    236         if version == 0 {
    237             return Err(SchemaCatalogContractError::InvalidVersionSequence);
    238         }
    239         let service_objects: Vec<_> = service_objects
    240             .into_iter()
    241             .take(MAX_SCHEMA_OBJECT_COUNT + 1)
    242             .collect();
    243         if service_objects.len() + shared_objects().len() > MAX_SCHEMA_OBJECT_COUNT {
    244             return Err(SchemaCatalogContractError::TooManyObjects);
    245         }
    246         validate_object_set(&service_objects)?;
    247         let mut objects = shared_objects();
    248         objects.extend(service_objects.iter().map(ObjectRef::from));
    249         let actual_digest = snapshot_digest(version, &objects);
    250         if actual_digest != expected_digest {
    251             return Err(SchemaCatalogContractError::SnapshotDigestMismatch);
    252         }
    253         Ok(Self {
    254             version,
    255             object_count: u32::try_from(objects.len()).expect("schema object bound fits in u32"),
    256             digest: actual_digest,
    257         })
    258     }
    259 
    260     /// Computes a snapshot digest for pin generation after validating the object set.
    261     pub fn computed_digest<I>(
    262         version: u32,
    263         service_objects: I,
    264     ) -> Result<SchemaDigest, SchemaCatalogContractError>
    265     where
    266         I: IntoIterator<Item = SchemaObject>,
    267     {
    268         if version == 0 {
    269             return Err(SchemaCatalogContractError::InvalidVersionSequence);
    270         }
    271         let service_objects: Vec<_> = service_objects
    272             .into_iter()
    273             .take(MAX_SCHEMA_OBJECT_COUNT + 1)
    274             .collect();
    275         if service_objects.len() + shared_objects().len() > MAX_SCHEMA_OBJECT_COUNT {
    276             return Err(SchemaCatalogContractError::TooManyObjects);
    277         }
    278         validate_object_set(&service_objects)?;
    279         let mut objects = shared_objects();
    280         objects.extend(service_objects.iter().map(ObjectRef::from));
    281         Ok(snapshot_digest(version, &objects))
    282     }
    283 
    284     #[must_use]
    285     pub const fn version(self) -> u32 {
    286         self.version
    287     }
    288 
    289     #[must_use]
    290     pub const fn object_count(self) -> u32 {
    291         self.object_count
    292     }
    293 
    294     #[must_use]
    295     pub const fn digest(self) -> SchemaDigest {
    296         self.digest
    297     }
    298 }
    299 
    300 /// Ordered exact schema snapshots bound to one migration catalog.
    301 #[derive(Clone, PartialEq, Eq)]
    302 pub struct SchemaCatalog {
    303     versions: Box<[SchemaVersionCatalog]>,
    304     migration_catalog_digest: MigrationChecksum,
    305     digest: SchemaDigest,
    306 }
    307 
    308 impl SchemaCatalog {
    309     /// Validates one exact snapshot for every migration-catalog schema version.
    310     pub fn new<I>(
    311         migrations: &MigrationCatalog,
    312         versions: I,
    313     ) -> Result<Self, SchemaCatalogContractError>
    314     where
    315         I: IntoIterator<Item = SchemaVersionCatalog>,
    316     {
    317         let versions: Vec<_> = versions
    318             .into_iter()
    319             .take(MAX_SCHEMA_VERSION_COUNT + 1)
    320             .collect();
    321         if versions.len() > MAX_SCHEMA_VERSION_COUNT {
    322             return Err(SchemaCatalogContractError::TooManyVersions);
    323         }
    324         let expected_len = usize::try_from(migrations.current_version())
    325             .map_err(|_| SchemaCatalogContractError::MigrationCatalogMismatch)?;
    326         if versions.len() != expected_len
    327             || versions
    328                 .iter()
    329                 .enumerate()
    330                 .any(|(index, entry)| entry.version != u32::try_from(index + 1).unwrap_or(0))
    331         {
    332             return Err(SchemaCatalogContractError::InvalidVersionSequence);
    333         }
    334         let migration_catalog_digest = migrations.digest();
    335         let digest = catalog_digest(migration_catalog_digest, &versions);
    336         Ok(Self {
    337             versions: versions.into_boxed_slice(),
    338             migration_catalog_digest,
    339             digest,
    340         })
    341     }
    342 
    343     #[must_use]
    344     pub fn versions(&self) -> &[SchemaVersionCatalog] {
    345         &self.versions
    346     }
    347 
    348     #[must_use]
    349     pub const fn migration_catalog_digest(&self) -> MigrationChecksum {
    350         self.migration_catalog_digest
    351     }
    352 
    353     #[must_use]
    354     pub const fn digest(&self) -> SchemaDigest {
    355         self.digest
    356     }
    357 
    358     #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    359     pub(crate) fn matches_migrations(&self, migrations: &MigrationCatalog) -> bool {
    360         crate::all_constraints([
    361             self.migration_catalog_digest == migrations.digest(),
    362             self.versions.len()
    363                 == usize::try_from(migrations.current_version()).unwrap_or(usize::MAX),
    364         ])
    365     }
    366 
    367     #[cfg(any(test, target_os = "linux", target_os = "macos"))]
    368     pub(crate) fn version(&self, version: u32) -> Option<SchemaVersionCatalog> {
    369         let index = usize::try_from(version.checked_sub(1)?).ok()?;
    370         self.versions.get(index).copied()
    371     }
    372 }
    373 
    374 impl fmt::Debug for SchemaCatalog {
    375     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    376         formatter
    377             .debug_struct("SchemaCatalog")
    378             .field("version_count", &self.versions.len())
    379             .field("migration_catalog_digest", &self.migration_catalog_digest)
    380             .field("digest", &self.digest)
    381             .finish()
    382     }
    383 }
    384 
    385 /// Invalid immutable schema-catalog construction.
    386 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    387 pub enum SchemaCatalogContractError {
    388     InvalidName,
    389     ReservedName,
    390     InvalidBinding,
    391     InvalidSql,
    392     ObjectDigestMismatch,
    393     DuplicateObject,
    394     TooManyObjects,
    395     SnapshotDigestMismatch,
    396     InvalidVersionSequence,
    397     TooManyVersions,
    398     MigrationCatalogMismatch,
    399 }
    400 
    401 impl fmt::Display for SchemaCatalogContractError {
    402     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    403         formatter.write_str(match self {
    404             Self::InvalidName => "SQLite schema object name is invalid",
    405             Self::ReservedName => "SQLite schema object name is reserved",
    406             Self::InvalidBinding => "SQLite schema object table binding is invalid",
    407             Self::InvalidSql => "SQLite schema object definition is invalid",
    408             Self::ObjectDigestMismatch => "SQLite schema object digest does not match",
    409             Self::DuplicateObject => "SQLite schema object identity is duplicated",
    410             Self::TooManyObjects => "SQLite schema snapshot has too many objects",
    411             Self::SnapshotDigestMismatch => "SQLite schema snapshot digest does not match",
    412             Self::InvalidVersionSequence => "SQLite schema catalog version sequence is invalid",
    413             Self::TooManyVersions => "SQLite schema catalog has too many versions",
    414             Self::MigrationCatalogMismatch => {
    415                 "SQLite schema catalog does not match the migration catalog"
    416             }
    417         })
    418     }
    419 }
    420 
    421 impl Error for SchemaCatalogContractError {}
    422 
    423 #[derive(Clone, Copy)]
    424 pub(crate) struct ObjectRef<'a> {
    425     pub(crate) kind: SchemaObjectKind,
    426     pub(crate) name: &'a str,
    427     pub(crate) table_name: &'a str,
    428     pub(crate) sql: &'a str,
    429     pub(crate) digest: SchemaDigest,
    430 }
    431 
    432 impl<'a> From<&'a SchemaObject> for ObjectRef<'a> {
    433     fn from(value: &'a SchemaObject) -> Self {
    434         Self {
    435             kind: value.kind,
    436             name: value.name,
    437             table_name: value.table_name,
    438             sql: value.sql,
    439             digest: value.digest,
    440         }
    441     }
    442 }
    443 
    444 pub(crate) fn object_digest(
    445     kind: SchemaObjectKind,
    446     name: &str,
    447     table_name: &str,
    448     sql: &str,
    449 ) -> SchemaDigest {
    450     let mut hasher = Sha256::new();
    451     hasher.update(OBJECT_DOMAIN);
    452     hasher.update([kind.tag()]);
    453     update_length_prefixed(&mut hasher, name.as_bytes());
    454     update_length_prefixed(&mut hasher, table_name.as_bytes());
    455     update_length_prefixed(&mut hasher, sql.as_bytes());
    456     SchemaDigest(hasher.finalize().into())
    457 }
    458 
    459 pub(crate) fn snapshot_digest(version: u32, objects: &[ObjectRef<'_>]) -> SchemaDigest {
    460     let mut objects = objects.to_vec();
    461     objects.sort_by(|left, right| {
    462         (
    463             left.kind.tag(),
    464             left.name.as_bytes(),
    465             left.table_name.as_bytes(),
    466         )
    467             .cmp(&(
    468                 right.kind.tag(),
    469                 right.name.as_bytes(),
    470                 right.table_name.as_bytes(),
    471             ))
    472     });
    473     let mut hasher = Sha256::new();
    474     hasher.update(SNAPSHOT_DOMAIN);
    475     hasher.update(version.to_be_bytes());
    476     hasher.update(
    477         u32::try_from(objects.len())
    478             .expect("schema object bound fits in u32")
    479             .to_be_bytes(),
    480     );
    481     for object in objects {
    482         hasher.update([object.kind.tag()]);
    483         update_length_prefixed(&mut hasher, object.name.as_bytes());
    484         update_length_prefixed(&mut hasher, object.table_name.as_bytes());
    485         hasher.update(object.digest.as_bytes());
    486     }
    487     SchemaDigest(hasher.finalize().into())
    488 }
    489 
    490 fn catalog_digest(
    491     migration_digest: MigrationChecksum,
    492     versions: &[SchemaVersionCatalog],
    493 ) -> SchemaDigest {
    494     let mut hasher = Sha256::new();
    495     hasher.update(CATALOG_DOMAIN);
    496     hasher.update(migration_digest.as_bytes());
    497     hasher.update(
    498         u32::try_from(versions.len())
    499             .expect("schema version bound fits in u32")
    500             .to_be_bytes(),
    501     );
    502     for version in versions {
    503         hasher.update(version.version.to_be_bytes());
    504         hasher.update(version.object_count.to_be_bytes());
    505         hasher.update(version.digest.as_bytes());
    506     }
    507     SchemaDigest(hasher.finalize().into())
    508 }
    509 
    510 fn update_length_prefixed(hasher: &mut Sha256, value: &[u8]) {
    511     hasher.update(
    512         u64::try_from(value.len())
    513             .expect("bounded schema field fits in u64")
    514             .to_be_bytes(),
    515     );
    516     hasher.update(value);
    517 }
    518 
    519 fn validate_service_object(
    520     kind: SchemaObjectKind,
    521     name: &str,
    522     table_name: &str,
    523     sql: &str,
    524 ) -> Result<(), SchemaCatalogContractError> {
    525     if !crate::all_constraints([valid_name(name), valid_name(table_name)]) {
    526         return Err(SchemaCatalogContractError::InvalidName);
    527     }
    528     if !crate::all_constraints([!is_reserved(name), !is_reserved(table_name)]) {
    529         return Err(SchemaCatalogContractError::ReservedName);
    530     }
    531     if (kind == SchemaObjectKind::Table) != (name == table_name) {
    532         return Err(SchemaCatalogContractError::InvalidBinding);
    533     }
    534     if !crate::all_constraints([
    535         !sql.is_empty(),
    536         sql.len() <= MAX_SCHEMA_SQL_UTF8_BYTES,
    537         !sql.as_bytes().contains(&0),
    538     ]) {
    539         return Err(SchemaCatalogContractError::InvalidSql);
    540     }
    541     Ok(())
    542 }
    543 
    544 fn validate_object_set(objects: &[SchemaObject]) -> Result<(), SchemaCatalogContractError> {
    545     let mut identities = BTreeSet::new();
    546     let tables = objects
    547         .iter()
    548         .filter(|object| object.kind == SchemaObjectKind::Table)
    549         .map(|object| object.name)
    550         .collect::<BTreeSet<_>>();
    551     let mut total_sql_bytes = shared_objects()
    552         .iter()
    553         .map(|object| object.sql.len())
    554         .sum::<usize>();
    555     for object in objects {
    556         if !identities.insert((object.kind, object.name)) {
    557             return Err(SchemaCatalogContractError::DuplicateObject);
    558         }
    559         if object.kind != SchemaObjectKind::Table && !tables.contains(object.table_name) {
    560             return Err(SchemaCatalogContractError::InvalidBinding);
    561         }
    562         total_sql_bytes = total_sql_bytes
    563             .checked_add(object.sql.len())
    564             .ok_or(SchemaCatalogContractError::TooManyObjects)?;
    565     }
    566     if total_sql_bytes > MAX_SCHEMA_CATALOG_UTF8_BYTES {
    567         return Err(SchemaCatalogContractError::TooManyObjects);
    568     }
    569     Ok(())
    570 }
    571 
    572 fn valid_name(value: &str) -> bool {
    573     let bytes = value.as_bytes();
    574     if bytes.is_empty() {
    575         return false;
    576     }
    577     crate::all_constraints([
    578         bytes.len() <= MAX_SCHEMA_NAME_UTF8_BYTES,
    579         bytes[0].is_ascii_lowercase(),
    580         bytes[bytes.len() - 1].is_ascii_alphanumeric(),
    581         !bytes.windows(2).any(|pair| pair == b"__"),
    582         bytes
    583             .iter()
    584             .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_'),
    585     ])
    586 }
    587 
    588 fn is_reserved(value: &str) -> bool {
    589     value.starts_with("sqlite_") || shared_objects().iter().any(|object| object.name == value)
    590 }
    591 
    592 fn shared_objects() -> Vec<ObjectRef<'static>> {
    593     [
    594         (
    595             SchemaObjectKind::Table,
    596             "radroots_service_metadata",
    597             "radroots_service_metadata",
    598             CREATE_METADATA_TABLE_SQL,
    599         ),
    600         (
    601             SchemaObjectKind::Trigger,
    602             "radroots_service_metadata_guard_update",
    603             "radroots_service_metadata",
    604             CREATE_METADATA_GUARD_TRIGGER_SQL,
    605         ),
    606         (
    607             SchemaObjectKind::Trigger,
    608             "radroots_service_metadata_no_delete",
    609             "radroots_service_metadata",
    610             CREATE_METADATA_NO_DELETE_TRIGGER_SQL,
    611         ),
    612         (
    613             SchemaObjectKind::Table,
    614             "schema_migrations",
    615             "schema_migrations",
    616             CREATE_MIGRATION_LEDGER_TABLE_SQL,
    617         ),
    618         (
    619             SchemaObjectKind::Trigger,
    620             "schema_migrations_no_update",
    621             "schema_migrations",
    622             CREATE_MIGRATION_NO_UPDATE_TRIGGER_SQL,
    623         ),
    624         (
    625             SchemaObjectKind::Trigger,
    626             "schema_migrations_no_delete",
    627             "schema_migrations",
    628             CREATE_MIGRATION_NO_DELETE_TRIGGER_SQL,
    629         ),
    630     ]
    631     .into_iter()
    632     .map(|(kind, name, table_name, sql)| ObjectRef {
    633         kind,
    634         name,
    635         table_name,
    636         sql,
    637         digest: object_digest(kind, name, table_name, sql),
    638     })
    639     .collect()
    640 }
    641 
    642 #[cfg(test)]
    643 mod tests {
    644     use super::*;
    645 
    646     const TABLE_SQL: &str = "CREATE TABLE alpha (value INTEGER NOT NULL) STRICT";
    647 
    648     fn table() -> SchemaObject {
    649         SchemaObject::new(
    650             SchemaObjectKind::Table,
    651             "alpha",
    652             "alpha",
    653             TABLE_SQL,
    654             SchemaObject::computed_digest(SchemaObjectKind::Table, "alpha", "alpha", TABLE_SQL)
    655                 .unwrap(),
    656         )
    657         .unwrap()
    658     }
    659 
    660     fn empty_migrations() -> MigrationCatalog {
    661         MigrationCatalog::new([]).unwrap()
    662     }
    663 
    664     #[test]
    665     fn aggregate_schema_sql_accepts_its_exact_limit_and_rejects_one_extra_byte() {
    666         fn object(name: &'static str, size: usize) -> SchemaObject {
    667             let prefix = format!("CREATE TABLE {name} (value INTEGER) /*");
    668             let sql: &'static str = Box::leak(
    669                 format!("{prefix}{}*/", "x".repeat(size - prefix.len() - 2)).into_boxed_str(),
    670             );
    671             assert_eq!(sql.len(), size);
    672             let digest =
    673                 SchemaObject::computed_digest(SchemaObjectKind::Table, name, name, sql).unwrap();
    674             SchemaObject::new(SchemaObjectKind::Table, name, name, sql, digest).unwrap()
    675         }
    676         let shared = shared_objects()
    677             .iter()
    678             .map(|object| object.sql.len())
    679             .sum::<usize>();
    680         let mut objects = (0..15)
    681             .map(|index| {
    682                 let name: &'static str = Box::leak(format!("table_{index}").into_boxed_str());
    683                 object(name, MAX_SCHEMA_SQL_UTF8_BYTES)
    684             })
    685             .collect::<Vec<_>>();
    686         let last_size = MAX_SCHEMA_CATALOG_UTF8_BYTES - shared - 15 * MAX_SCHEMA_SQL_UTF8_BYTES;
    687         objects.push(object("last_table", last_size));
    688         SchemaVersionCatalog::computed_digest(1, objects.iter().cloned()).unwrap();
    689         *objects.last_mut().unwrap() = object("last_table", last_size + 1);
    690         assert_eq!(
    691             SchemaVersionCatalog::computed_digest(1, objects),
    692             Err(SchemaCatalogContractError::TooManyObjects)
    693         );
    694     }
    695 
    696     #[test]
    697     fn exact_object_snapshot_and_catalog_vectors_are_stable() {
    698         let object = table();
    699         assert_eq!(
    700             object.digest().as_bytes(),
    701             &[
    702                 0xf1, 0xa0, 0x6b, 0x60, 0x76, 0x0f, 0x73, 0xae, 0x0b, 0x43, 0x44, 0x1b, 0xb8, 0xfc,
    703                 0x12, 0x56, 0x48, 0xa9, 0xcb, 0xf5, 0x63, 0xdb, 0x59, 0xd6, 0xc1, 0xfc, 0x60, 0x3b,
    704                 0x5e, 0x92, 0x0b, 0x7c,
    705             ]
    706         );
    707         let snapshot_digest = SchemaVersionCatalog::computed_digest(1, [object.clone()]).unwrap();
    708         assert_eq!(
    709             snapshot_digest.as_bytes(),
    710             &[
    711                 0x9e, 0x50, 0xd2, 0x25, 0xfe, 0xdc, 0xe1, 0x4f, 0x0d, 0x40, 0x41, 0x84, 0xb2, 0x00,
    712                 0xbc, 0xd5, 0xcf, 0xe9, 0xb3, 0x56, 0x98, 0x16, 0xf6, 0x29, 0xc2, 0x40, 0x86, 0xdf,
    713                 0x93, 0x2f, 0x52, 0x4a,
    714             ]
    715         );
    716         let version = SchemaVersionCatalog::new(1, [object], snapshot_digest).unwrap();
    717         let catalog = SchemaCatalog::new(&empty_migrations(), [version]).unwrap();
    718         assert_eq!(version.version(), 1);
    719         assert_eq!(version.object_count(), 7);
    720         assert_eq!(catalog.versions(), &[version]);
    721         assert_eq!(
    722             catalog.digest().as_bytes(),
    723             &[
    724                 0xff, 0x9d, 0xbe, 0x4f, 0x32, 0x42, 0xb3, 0x3f, 0x6f, 0xd8, 0x76, 0x9d, 0x3e, 0x11,
    725                 0x21, 0x7b, 0x38, 0xb2, 0x77, 0x3e, 0xa6, 0xa2, 0x98, 0x6b, 0x85, 0xb6, 0xed, 0x8d,
    726                 0xe2, 0x4b, 0x52, 0x51,
    727             ]
    728         );
    729     }
    730 
    731     #[test]
    732     fn object_validation_is_closed_and_redacted() {
    733         let digest = SchemaDigest::from_bytes([0; 32]);
    734         for result in [
    735             SchemaObject::new(SchemaObjectKind::Table, "", "", "x", digest),
    736             SchemaObject::new(SchemaObjectKind::Table, "Bad", "Bad", "x", digest),
    737             SchemaObject::new(
    738                 SchemaObjectKind::Table,
    739                 "sqlite_bad",
    740                 "sqlite_bad",
    741                 "x",
    742                 digest,
    743             ),
    744             SchemaObject::new(
    745                 SchemaObjectKind::Table,
    746                 "schema_migrations",
    747                 "schema_migrations",
    748                 "x",
    749                 digest,
    750             ),
    751             SchemaObject::new(
    752                 SchemaObjectKind::Index,
    753                 "alpha_idx",
    754                 "alpha_idx",
    755                 "x",
    756                 digest,
    757             ),
    758             SchemaObject::new(SchemaObjectKind::Table, "alpha", "alpha", "", digest),
    759         ] {
    760             assert!(result.is_err());
    761         }
    762         let debug = format!("{:?}", table());
    763         assert!(!debug.contains(TABLE_SQL));
    764         assert!(debug.contains("[redacted]"));
    765 
    766         let maximum_name = Box::leak("a".repeat(MAX_SCHEMA_NAME_UTF8_BYTES).into_boxed_str());
    767         let maximum_digest =
    768             SchemaObject::computed_digest(SchemaObjectKind::Table, maximum_name, maximum_name, "x")
    769                 .unwrap();
    770         assert!(
    771             SchemaObject::new(
    772                 SchemaObjectKind::Table,
    773                 maximum_name,
    774                 maximum_name,
    775                 "x",
    776                 maximum_digest,
    777             )
    778             .is_ok()
    779         );
    780         let excessive_name = Box::leak("a".repeat(MAX_SCHEMA_NAME_UTF8_BYTES + 1).into_boxed_str());
    781         assert_eq!(
    782             SchemaObject::computed_digest(
    783                 SchemaObjectKind::Table,
    784                 excessive_name,
    785                 excessive_name,
    786                 "x",
    787             ),
    788             Err(SchemaCatalogContractError::InvalidName)
    789         );
    790         assert_eq!(
    791             SchemaObject::computed_digest(
    792                 SchemaObjectKind::Table,
    793                 "alpha__beta",
    794                 "alpha__beta",
    795                 "x",
    796             ),
    797             Err(SchemaCatalogContractError::InvalidName)
    798         );
    799         for invalid in ["2alpha", "alpha_", "alpha-beta"] {
    800             assert_eq!(
    801                 SchemaObject::computed_digest(SchemaObjectKind::Table, invalid, invalid, "x"),
    802                 Err(SchemaCatalogContractError::InvalidName)
    803             );
    804         }
    805 
    806         let maximum_sql = Box::leak("x".repeat(MAX_SCHEMA_SQL_UTF8_BYTES).into_boxed_str());
    807         assert!(
    808             SchemaObject::computed_digest(
    809                 SchemaObjectKind::Table,
    810                 "maximum_sql",
    811                 "maximum_sql",
    812                 maximum_sql,
    813             )
    814             .is_ok()
    815         );
    816         let excessive_sql = Box::leak("x".repeat(MAX_SCHEMA_SQL_UTF8_BYTES + 1).into_boxed_str());
    817         assert_eq!(
    818             SchemaObject::computed_digest(
    819                 SchemaObjectKind::Table,
    820                 "excessive_sql",
    821                 "excessive_sql",
    822                 excessive_sql,
    823             ),
    824             Err(SchemaCatalogContractError::InvalidSql)
    825         );
    826         assert_eq!(
    827             SchemaObject::computed_digest(SchemaObjectKind::Table, "nul_sql", "nul_sql", "x\0y",),
    828             Err(SchemaCatalogContractError::InvalidSql)
    829         );
    830 
    831         assert_eq!(
    832             SchemaObject::new(
    833                 SchemaObjectKind::Table,
    834                 "alpha",
    835                 "alpha",
    836                 TABLE_SQL,
    837                 SchemaDigest::from_bytes([0; 32]),
    838             ),
    839             Err(SchemaCatalogContractError::ObjectDigestMismatch)
    840         );
    841     }
    842 
    843     #[test]
    844     fn object_sets_reject_duplicates_missing_tables_and_bounds() {
    845         let duplicate_digest = SchemaVersionCatalog::computed_digest(1, [table(), table()]);
    846         assert_eq!(
    847             duplicate_digest,
    848             Err(SchemaCatalogContractError::DuplicateObject)
    849         );
    850 
    851         const INDEX_SQL: &str = "CREATE INDEX alpha_idx ON missing(value)";
    852         let index = SchemaObject::new(
    853             SchemaObjectKind::Index,
    854             "alpha_idx",
    855             "missing",
    856             INDEX_SQL,
    857             SchemaObject::computed_digest(
    858                 SchemaObjectKind::Index,
    859                 "alpha_idx",
    860                 "missing",
    861                 INDEX_SQL,
    862             )
    863             .unwrap(),
    864         )
    865         .unwrap();
    866         assert_eq!(
    867             SchemaVersionCatalog::computed_digest(1, [index]),
    868             Err(SchemaCatalogContractError::InvalidBinding)
    869         );
    870 
    871         let excessive = std::iter::repeat_with(table).take(MAX_SCHEMA_OBJECT_COUNT + 1);
    872         assert_eq!(
    873             SchemaVersionCatalog::computed_digest(1, excessive),
    874             Err(SchemaCatalogContractError::TooManyObjects)
    875         );
    876         let infinite = std::iter::repeat_with(table);
    877         assert_eq!(
    878             SchemaVersionCatalog::computed_digest(1, infinite),
    879             Err(SchemaCatalogContractError::TooManyObjects)
    880         );
    881 
    882         let maximum = (0..(MAX_SCHEMA_OBJECT_COUNT - shared_objects().len()))
    883             .map(|index| {
    884                 let name = Box::leak(format!("table_{index}").into_boxed_str());
    885                 let sql =
    886                     Box::leak(format!("CREATE TABLE {name} (value INTEGER)").into_boxed_str());
    887                 SchemaObject::new(
    888                     SchemaObjectKind::Table,
    889                     name,
    890                     name,
    891                     sql,
    892                     SchemaObject::computed_digest(SchemaObjectKind::Table, name, name, sql)
    893                         .unwrap(),
    894                 )
    895                 .unwrap()
    896             })
    897             .collect::<Vec<_>>();
    898         assert!(SchemaVersionCatalog::computed_digest(1, maximum.iter().cloned()).is_ok());
    899         let mut excessive = maximum;
    900         excessive.push(table());
    901         assert_eq!(
    902             SchemaVersionCatalog::computed_digest(1, excessive),
    903             Err(SchemaCatalogContractError::TooManyObjects)
    904         );
    905     }
    906 
    907     #[test]
    908     fn catalog_requires_every_exact_migration_version_and_terminates() {
    909         let migrations = empty_migrations();
    910         let digest = SchemaVersionCatalog::computed_digest(1, []).unwrap();
    911         let v1 = SchemaVersionCatalog::new(1, [], digest).unwrap();
    912         assert_eq!(
    913             SchemaVersionCatalog::new(0, [], SchemaDigest::from_bytes([0; 32])),
    914             Err(SchemaCatalogContractError::InvalidVersionSequence)
    915         );
    916         assert_eq!(
    917             SchemaVersionCatalog::computed_digest(0, []),
    918             Err(SchemaCatalogContractError::InvalidVersionSequence)
    919         );
    920         assert_eq!(
    921             SchemaVersionCatalog::new(1, [], SchemaDigest::from_bytes([0; 32])),
    922             Err(SchemaCatalogContractError::SnapshotDigestMismatch)
    923         );
    924         assert!(SchemaCatalog::new(&migrations, [v1]).is_ok());
    925         assert_eq!(
    926             SchemaCatalog::new(&migrations, []),
    927             Err(SchemaCatalogContractError::InvalidVersionSequence)
    928         );
    929         let infinite = std::iter::repeat(v1);
    930         assert_eq!(
    931             SchemaCatalog::new(&migrations, infinite),
    932             Err(SchemaCatalogContractError::TooManyVersions)
    933         );
    934 
    935         let descriptors = (0..4096_u32)
    936             .map(|index| {
    937                 let name = Box::leak(format!("migration_{index}").into_boxed_str());
    938                 crate::MigrationDescriptor::callback(
    939                     index + 2,
    940                     name,
    941                     b"x",
    942                     MigrationChecksum::for_callback(b"x"),
    943                 )
    944                 .unwrap()
    945             })
    946             .collect::<Vec<_>>();
    947         let maximum_migrations = MigrationCatalog::new(descriptors).unwrap();
    948         let maximum_versions = (1..=4097_u32)
    949             .map(|version| {
    950                 let digest = SchemaVersionCatalog::computed_digest(version, []).unwrap();
    951                 SchemaVersionCatalog::new(version, [], digest).unwrap()
    952             })
    953             .collect::<Vec<_>>();
    954         assert!(SchemaCatalog::new(&maximum_migrations, maximum_versions).is_ok());
    955 
    956         let digest = SchemaVersionCatalog::computed_digest(1, []).unwrap();
    957         let v1 = SchemaVersionCatalog::new(1, [], digest).unwrap();
    958         let duplicate = SchemaVersionCatalog { version: 1, ..v1 };
    959         assert_eq!(
    960             SchemaCatalog::new(&maximum_migrations, [v1, duplicate]),
    961             Err(SchemaCatalogContractError::InvalidVersionSequence)
    962         );
    963 
    964         let excessive_objects = std::iter::repeat_with(table).take(MAX_SCHEMA_OBJECT_COUNT + 1);
    965         assert_eq!(
    966             SchemaVersionCatalog::new(1, excessive_objects, SchemaDigest::from_bytes([0; 32])),
    967             Err(SchemaCatalogContractError::TooManyObjects)
    968         );
    969 
    970         let gap_v2 = SchemaVersionCatalog { version: 2, ..v1 };
    971         assert_eq!(
    972             SchemaCatalog::new(&migrations, [gap_v2]),
    973             Err(SchemaCatalogContractError::InvalidVersionSequence)
    974         );
    975 
    976         let v1_for_exact = v1;
    977         let exact = SchemaCatalog::new(&migrations, [v1_for_exact]).expect("exact catalog");
    978         assert!(exact.matches_migrations(&migrations));
    979         assert_eq!(exact.version(0), None);
    980         assert_eq!(exact.version(1), Some(v1));
    981         assert_eq!(exact.version(2), None);
    982 
    983         let callback = crate::MigrationDescriptor::callback(
    984             2,
    985             "next_schema",
    986             b"definition",
    987             MigrationChecksum::for_callback(b"definition"),
    988         )
    989         .expect("migration");
    990         let other_migrations = MigrationCatalog::new([callback]).expect("other migrations");
    991         assert!(!exact.matches_migrations(&other_migrations));
    992     }
    993 }