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 }