mod.rs (23039B)
1 //! Exact schema-object catalog verification. 2 3 pub(crate) mod catalog; 4 mod inspection; 5 6 pub use catalog::{ 7 SchemaCatalog, SchemaCatalogContractError, SchemaDigest, SchemaObject, SchemaObjectKind, 8 SchemaVersionCatalog, 9 }; 10 pub use inspection::{ 11 IntegrityCheckOutcome, IntegrityCheckedAtUnixMs, IntegrityDiagnosticCode, 12 ServiceSqliteIntegrityReport, 13 }; 14 15 #[cfg(any(target_os = "linux", target_os = "macos"))] 16 pub(crate) use inspection::inspect_database_integrity; 17 18 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] 19 pub(crate) use inspection::test_seam as integrity_test_seam; 20 21 #[cfg(any(target_os = "linux", target_os = "macos"))] 22 use core::fmt; 23 #[cfg(any(target_os = "linux", target_os = "macos"))] 24 use std::error::Error; 25 26 #[cfg(any(target_os = "linux", target_os = "macos"))] 27 use sqlx::{Row, SqliteConnection}; 28 29 #[cfg(any(target_os = "linux", target_os = "macos"))] 30 use crate::{ServiceSqliteError, ServiceSqliteErrorKind}; 31 32 #[cfg(any(target_os = "linux", target_os = "macos"))] 33 use self::catalog::{ 34 MAX_SCHEMA_CATALOG_UTF8_BYTES, MAX_SCHEMA_OBJECT_COUNT, MAX_SCHEMA_SQL_UTF8_BYTES, ObjectRef, 35 object_digest, snapshot_digest, 36 }; 37 38 #[cfg(any(target_os = "linux", target_os = "macos"))] 39 const READ_SCHEMA_CATALOG_SQL: &str = r#" 40 WITH raw_catalog AS ( 41 SELECT type, name, tbl_name, sql 42 FROM main.sqlite_schema 43 WHERE NOT (typeof(name) = 'text' AND substr(name, 1, 7) = 'sqlite_') 44 ), bounded_catalog AS ( 45 SELECT 46 type, 47 name, 48 tbl_name, 49 sql, 50 COUNT(*) OVER () AS object_count, 51 COALESCE(SUM(length(CAST(sql AS BLOB))) OVER (), 0) AS total_sql_bytes 52 FROM raw_catalog 53 ) 54 SELECT 55 object_count, 56 total_sql_bytes, 57 CASE 58 WHEN object_count <= 4096 59 AND typeof(type) = 'text' 60 AND length(CAST(type AS BLOB)) BETWEEN 1 AND 16 61 THEN type 62 END AS bounded_type, 63 CASE 64 WHEN object_count <= 4096 65 AND typeof(name) = 'text' 66 AND length(CAST(name AS BLOB)) BETWEEN 1 AND 128 67 THEN name 68 END AS bounded_name, 69 CASE 70 WHEN object_count <= 4096 71 AND typeof(tbl_name) = 'text' 72 AND length(CAST(tbl_name AS BLOB)) BETWEEN 1 AND 128 73 THEN tbl_name 74 END AS bounded_table_name, 75 CASE 76 WHEN object_count <= 4096 77 AND total_sql_bytes <= 16777216 78 AND typeof(sql) = 'text' 79 AND length(CAST(sql AS BLOB)) BETWEEN 1 AND 1048576 80 THEN sql 81 END AS bounded_sql 82 FROM bounded_catalog 83 LIMIT 4097 84 "#; 85 86 #[cfg(any(target_os = "linux", target_os = "macos"))] 87 #[derive(Clone, Debug, PartialEq, Eq)] 88 pub(crate) struct SchemaVerificationReport { 89 version: u32, 90 expected_count: u32, 91 actual_count: u32, 92 expected_digest: SchemaDigest, 93 actual_digest: SchemaDigest, 94 } 95 96 #[cfg(any(target_os = "linux", target_os = "macos"))] 97 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 98 enum SchemaIntegrityFailureKind { 99 CatalogMismatch, 100 CatalogCorrupt, 101 } 102 103 #[cfg(any(target_os = "linux", target_os = "macos"))] 104 struct SchemaIntegrityFailure { 105 kind: SchemaIntegrityFailureKind, 106 report: Option<SchemaVerificationReport>, 107 } 108 109 #[cfg(any(target_os = "linux", target_os = "macos"))] 110 impl fmt::Debug for SchemaIntegrityFailure { 111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 112 formatter 113 .debug_struct("SchemaIntegrityFailure") 114 .field("kind", &self.kind) 115 .field("report", &self.report) 116 .finish() 117 } 118 } 119 120 #[cfg(any(target_os = "linux", target_os = "macos"))] 121 impl fmt::Display for SchemaIntegrityFailure { 122 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 123 formatter.write_str(match self.kind { 124 SchemaIntegrityFailureKind::CatalogMismatch => { 125 "SQLite schema object catalog does not match" 126 } 127 SchemaIntegrityFailureKind::CatalogCorrupt => "SQLite schema object catalog is invalid", 128 }) 129 } 130 } 131 132 #[cfg(any(target_os = "linux", target_os = "macos"))] 133 impl Error for SchemaIntegrityFailure {} 134 135 #[cfg(any(target_os = "linux", target_os = "macos"))] 136 fn integrity_error(kind: SchemaIntegrityFailureKind) -> ServiceSqliteError { 137 ServiceSqliteError::with_source( 138 ServiceSqliteErrorKind::Integrity, 139 SchemaIntegrityFailure { kind, report: None }, 140 ) 141 } 142 143 #[cfg(any(target_os = "linux", target_os = "macos"))] 144 fn mismatch_error(report: SchemaVerificationReport) -> ServiceSqliteError { 145 ServiceSqliteError::with_source( 146 ServiceSqliteErrorKind::Integrity, 147 SchemaIntegrityFailure { 148 kind: SchemaIntegrityFailureKind::CatalogMismatch, 149 report: Some(report), 150 }, 151 ) 152 } 153 154 #[cfg(any(target_os = "linux", target_os = "macos"))] 155 struct RuntimeSchemaObject { 156 kind: SchemaObjectKind, 157 name: String, 158 table_name: String, 159 sql: String, 160 digest: SchemaDigest, 161 } 162 163 #[cfg(any(target_os = "linux", target_os = "macos"))] 164 pub(crate) async fn verify_schema_catalog( 165 connection: &mut SqliteConnection, 166 catalog: &SchemaCatalog, 167 version: u32, 168 ) -> Result<SchemaVerificationReport, ServiceSqliteError> { 169 let expected = catalog 170 .version(version) 171 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogMismatch))?; 172 let rows = sqlx::query(READ_SCHEMA_CATALOG_SQL) 173 .fetch_all(connection) 174 .await 175 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 176 require_schema_row_limit(rows.len())?; 177 let mut objects = Vec::with_capacity(rows.len()); 178 let mut reported_count = None; 179 let mut reported_total = None; 180 for row in rows { 181 let count = row 182 .try_get::<i64, _>("object_count") 183 .ok() 184 .and_then(|value| u32::try_from(value).ok()) 185 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 186 let total = row 187 .try_get::<i64, _>("total_sql_bytes") 188 .ok() 189 .and_then(|value| usize::try_from(value).ok()) 190 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 191 require_catalog_summary_projection([ 192 count <= u32::try_from(MAX_SCHEMA_OBJECT_COUNT).unwrap_or(u32::MAX), 193 total <= MAX_SCHEMA_CATALOG_UTF8_BYTES, 194 reported_count.is_none_or(|observed| observed == count), 195 reported_total.is_none_or(|observed| observed == total), 196 ])?; 197 reported_count = Some(count); 198 reported_total = Some(total); 199 let object_type = row 200 .try_get::<String, _>("bounded_type") 201 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 202 let name = row 203 .try_get::<String, _>("bounded_name") 204 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 205 let table_name = row 206 .try_get::<String, _>("bounded_table_name") 207 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 208 let sql = row 209 .try_get::<String, _>("bounded_sql") 210 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 211 require_schema_sql_limit(sql.len())?; 212 let kind = SchemaObjectKind::from_sqlite(&object_type) 213 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 214 require_runtime_object_projection(kind, &name, &table_name)?; 215 let digest = object_digest(kind, &name, &table_name, &sql); 216 objects.push(RuntimeSchemaObject { 217 kind, 218 name, 219 table_name, 220 sql, 221 digest, 222 }); 223 } 224 let actual_count = u32::try_from(objects.len()) 225 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 226 require_reported_object_count(reported_count, actual_count)?; 227 let mut identities = std::collections::BTreeSet::new(); 228 require_unique_runtime_objects( 229 !objects 230 .iter() 231 .any(|object| !identities.insert((object.kind, object.name.as_str()))), 232 )?; 233 let refs = objects 234 .iter() 235 .map(|object| ObjectRef { 236 kind: object.kind, 237 name: &object.name, 238 table_name: &object.table_name, 239 sql: &object.sql, 240 digest: object.digest, 241 }) 242 .collect::<Vec<_>>(); 243 let actual_digest = snapshot_digest(version, &refs); 244 let report = SchemaVerificationReport { 245 version, 246 expected_count: expected.object_count(), 247 actual_count, 248 expected_digest: expected.digest(), 249 actual_digest, 250 }; 251 require_schema_report_projection(report) 252 } 253 254 #[cfg(any(target_os = "linux", target_os = "macos"))] 255 fn require_catalog_summary_projection(matches: [bool; 4]) -> Result<(), ServiceSqliteError> { 256 crate::all_constraints(matches) 257 .then_some(()) 258 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt)) 259 } 260 261 #[cfg(any(target_os = "linux", target_os = "macos"))] 262 fn require_runtime_object_projection( 263 kind: SchemaObjectKind, 264 name: &str, 265 table_name: &str, 266 ) -> Result<(), ServiceSqliteError> { 267 crate::all_constraints([ 268 runtime_name_is_valid(name), 269 runtime_name_is_valid(table_name), 270 (kind == SchemaObjectKind::Table) == (name == table_name), 271 ]) 272 .then_some(()) 273 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt)) 274 } 275 276 #[cfg(any(target_os = "linux", target_os = "macos"))] 277 fn require_schema_report_projection( 278 report: SchemaVerificationReport, 279 ) -> Result<SchemaVerificationReport, ServiceSqliteError> { 280 let matches = crate::all_constraints([ 281 report.expected_count == report.actual_count, 282 report.expected_digest == report.actual_digest, 283 ]); 284 if matches { 285 Ok(report) 286 } else { 287 Err(mismatch_error(report)) 288 } 289 } 290 291 #[cfg(any(target_os = "linux", target_os = "macos"))] 292 pub(crate) async fn verify_database_integrity( 293 connection: &mut SqliteConnection, 294 ) -> Result<(), ServiceSqliteError> { 295 let rows = sqlx::query(crate::persisted_value::INTEGRITY_CHECK_SQL) 296 .fetch_all(&mut *connection) 297 .await 298 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 299 let value = rows 300 .first() 301 .and_then(crate::persisted_value::bounded_integrity_bytes); 302 catalog_corrupt_unless(integrity_projection_matches(rows.len(), value))?; 303 let foreign_key_violation = 304 sqlx::query_scalar::<_, i64>("SELECT 1 FROM pragma_foreign_key_check LIMIT 1") 305 .fetch_optional(connection) 306 .await 307 .map_err(|_| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt))?; 308 require_no_foreign_key_violation(foreign_key_violation.is_some()) 309 } 310 311 #[cfg(any(target_os = "linux", target_os = "macos"))] 312 fn catalog_corrupt_unless(condition: bool) -> Result<(), ServiceSqliteError> { 313 condition 314 .then_some(()) 315 .ok_or_else(|| integrity_error(SchemaIntegrityFailureKind::CatalogCorrupt)) 316 } 317 318 #[cfg(any(target_os = "linux", target_os = "macos"))] 319 fn require_schema_row_limit(row_count: usize) -> Result<(), ServiceSqliteError> { 320 catalog_corrupt_unless(row_count <= MAX_SCHEMA_OBJECT_COUNT) 321 } 322 323 #[cfg(any(target_os = "linux", target_os = "macos"))] 324 fn require_schema_sql_limit(sql_bytes: usize) -> Result<(), ServiceSqliteError> { 325 catalog_corrupt_unless(sql_bytes <= MAX_SCHEMA_SQL_UTF8_BYTES) 326 } 327 328 #[cfg(any(target_os = "linux", target_os = "macos"))] 329 fn require_reported_object_count( 330 reported: Option<u32>, 331 actual: u32, 332 ) -> Result<(), ServiceSqliteError> { 333 catalog_corrupt_unless(reported.unwrap_or(0) == actual) 334 } 335 336 #[cfg(any(target_os = "linux", target_os = "macos"))] 337 fn require_unique_runtime_objects(unique: bool) -> Result<(), ServiceSqliteError> { 338 catalog_corrupt_unless(unique) 339 } 340 341 #[cfg(any(target_os = "linux", target_os = "macos"))] 342 fn require_no_foreign_key_violation(present: bool) -> Result<(), ServiceSqliteError> { 343 catalog_corrupt_unless(!present) 344 } 345 346 #[cfg(any(target_os = "linux", target_os = "macos"))] 347 fn integrity_projection_matches(row_count: usize, value: Option<&[u8]>) -> bool { 348 let present = value.is_some(); 349 let bounded = value 350 .is_some_and(|value| value.len() <= crate::persisted_value::MAX_INTEGRITY_RESULT_BYTES); 351 let exact = value == Some(b"ok".as_slice()); 352 crate::all_constraints([row_count == 1, present, bounded, exact]) 353 } 354 355 #[cfg(any(target_os = "linux", target_os = "macos"))] 356 fn runtime_name_is_valid(value: &str) -> bool { 357 let bytes = value.as_bytes(); 358 if bytes.is_empty() { 359 return false; 360 } 361 crate::all_constraints([ 362 bytes.len() <= 128, 363 bytes[0].is_ascii_lowercase(), 364 bytes[bytes.len() - 1].is_ascii_alphanumeric(), 365 !bytes.windows(2).any(|pair| pair == b"__"), 366 bytes 367 .iter() 368 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'_'), 369 ]) 370 } 371 372 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] 373 mod tests { 374 375 use super::*; 376 use sqlx::{Connection, Executor, sqlite::SqliteConnectOptions}; 377 378 const TABLE_SQL: &str = 379 "CREATE TABLE alpha (id INTEGER PRIMARY KEY, value INTEGER NOT NULL) STRICT"; 380 const INDEX_SQL: &str = "CREATE INDEX alpha_value_idx ON alpha(value)"; 381 const TRIGGER_SQL: &str = "CREATE TRIGGER alpha_guard BEFORE UPDATE ON alpha BEGIN SELECT RAISE(ABORT, 'blocked'); END"; 382 383 #[test] 384 fn schema_integrity_failure_inventory_is_complete_and_source_free() { 385 for (kind, message) in [ 386 ( 387 SchemaIntegrityFailureKind::CatalogMismatch, 388 "SQLite schema object catalog does not match", 389 ), 390 ( 391 SchemaIntegrityFailureKind::CatalogCorrupt, 392 "SQLite schema object catalog is invalid", 393 ), 394 ] { 395 let failure = SchemaIntegrityFailure { kind, report: None }; 396 assert_eq!(failure.to_string(), message); 397 assert!(failure.source().is_none()); 398 assert!(format!("{failure:?}").contains(&format!("{kind:?}"))); 399 let error = integrity_error(kind); 400 assert_eq!(error.kind(), ServiceSqliteErrorKind::Integrity); 401 assert!(error.source().is_some()); 402 } 403 } 404 405 #[test] 406 fn database_integrity_projection_rejects_each_independent_drift() { 407 assert!(integrity_projection_matches(1, Some(b"ok"))); 408 assert!(!integrity_projection_matches(0, Some(b"ok"))); 409 assert!(!integrity_projection_matches(2, Some(b"ok"))); 410 assert!(!integrity_projection_matches(1, None)); 411 assert!(!integrity_projection_matches(1, Some(b"not-ok"))); 412 let oversized = [b'x'; 65]; 413 assert!(!integrity_projection_matches(1, Some(&oversized))); 414 } 415 416 #[test] 417 fn catalog_projection_helpers_reject_every_independent_drift() { 418 assert!(require_catalog_summary_projection([true; 4]).is_ok()); 419 for changed in 0..4 { 420 let mut matches = [true; 4]; 421 matches[changed] = false; 422 assert!(require_catalog_summary_projection(matches).is_err()); 423 } 424 425 assert!( 426 require_runtime_object_projection(SchemaObjectKind::Table, "alpha", "alpha").is_ok() 427 ); 428 assert!( 429 require_runtime_object_projection(SchemaObjectKind::Index, "alpha_idx", "alpha") 430 .is_ok() 431 ); 432 for (kind, name, table) in [ 433 (SchemaObjectKind::Table, "Bad", "Bad"), 434 (SchemaObjectKind::Table, "alpha", "Bad"), 435 (SchemaObjectKind::Table, "alpha", "other"), 436 (SchemaObjectKind::Index, "alpha", "alpha"), 437 ] { 438 assert!(require_runtime_object_projection(kind, name, table).is_err()); 439 } 440 441 let digest = SchemaDigest::from_bytes([7; 32]); 442 let mut report = SchemaVerificationReport { 443 version: 1, 444 expected_count: 1, 445 actual_count: 1, 446 expected_digest: digest, 447 actual_digest: digest, 448 }; 449 assert!(require_schema_report_projection(report.clone()).is_ok()); 450 report.actual_count = 2; 451 assert!(require_schema_report_projection(report.clone()).is_err()); 452 report.actual_count = 1; 453 report.actual_digest = SchemaDigest::from_bytes([8; 32]); 454 assert!(require_schema_report_projection(report).is_err()); 455 456 assert!(catalog_corrupt_unless(true).is_ok()); 457 assert!(catalog_corrupt_unless(false).is_err()); 458 assert!(require_schema_row_limit(MAX_SCHEMA_OBJECT_COUNT).is_ok()); 459 assert!(require_schema_row_limit(MAX_SCHEMA_OBJECT_COUNT + 1).is_err()); 460 assert!(require_schema_sql_limit(MAX_SCHEMA_SQL_UTF8_BYTES).is_ok()); 461 assert!(require_schema_sql_limit(MAX_SCHEMA_SQL_UTF8_BYTES + 1).is_err()); 462 assert!(require_reported_object_count(Some(1), 1).is_ok()); 463 assert!(require_reported_object_count(None, 1).is_err()); 464 assert!(require_reported_object_count(Some(2), 1).is_err()); 465 assert!(require_unique_runtime_objects(true).is_ok()); 466 assert!(require_unique_runtime_objects(false).is_err()); 467 assert!(require_no_foreign_key_violation(false).is_ok()); 468 assert!(require_no_foreign_key_violation(true).is_err()); 469 } 470 471 fn object( 472 kind: SchemaObjectKind, 473 name: &'static str, 474 table_name: &'static str, 475 sql: &'static str, 476 ) -> SchemaObject { 477 SchemaObject::new( 478 kind, 479 name, 480 table_name, 481 sql, 482 SchemaObject::computed_digest(kind, name, table_name, sql).unwrap(), 483 ) 484 .unwrap() 485 } 486 487 fn expected(objects: Vec<SchemaObject>) -> SchemaCatalog { 488 let migrations = crate::MigrationCatalog::new([]).unwrap(); 489 let digest = SchemaVersionCatalog::computed_digest(1, objects.iter().cloned()).unwrap(); 490 let version = SchemaVersionCatalog::new(1, objects, digest).unwrap(); 491 SchemaCatalog::new(&migrations, [version]).unwrap() 492 } 493 494 fn full_objects() -> Vec<SchemaObject> { 495 vec![ 496 object( 497 SchemaObjectKind::Trigger, 498 "alpha_guard", 499 "alpha", 500 TRIGGER_SQL, 501 ), 502 object( 503 SchemaObjectKind::Index, 504 "alpha_value_idx", 505 "alpha", 506 INDEX_SQL, 507 ), 508 object(SchemaObjectKind::Table, "alpha", "alpha", TABLE_SQL), 509 ] 510 } 511 512 async fn shared_database() -> SqliteConnection { 513 let mut connection = 514 SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(":memory:")) 515 .await 516 .unwrap(); 517 for statement in catalog::METADATA_SCHEMA_SQL 518 .into_iter() 519 .chain(catalog::MIGRATION_LEDGER_SCHEMA_SQL) 520 { 521 connection.execute(statement).await.unwrap(); 522 } 523 connection 524 } 525 526 async fn full_database() -> SqliteConnection { 527 let mut connection = shared_database().await; 528 for statement in [TABLE_SQL, INDEX_SQL, TRIGGER_SQL] { 529 connection.execute(statement).await.unwrap(); 530 } 531 connection 532 } 533 534 #[tokio::test(flavor = "current_thread")] 535 async fn exact_catalog_is_order_independent_and_report_is_bounded() { 536 let mut connection = full_database().await; 537 let catalog = expected(full_objects()); 538 let first = verify_schema_catalog(&mut connection, &catalog, 1) 539 .await 540 .unwrap(); 541 let second = verify_schema_catalog(&mut connection, &catalog, 1) 542 .await 543 .unwrap(); 544 assert_eq!(first, second); 545 assert_eq!(first.version, 1); 546 assert_eq!(first.expected_count, 9); 547 assert_eq!(first.actual_count, 9); 548 assert_eq!(first.expected_digest, first.actual_digest); 549 assert!(!format!("{first:?}").contains(TABLE_SQL)); 550 } 551 552 #[tokio::test(flavor = "current_thread")] 553 async fn missing_extra_replaced_index_trigger_column_and_view_fail_closed() { 554 let cases = [ 555 "DROP INDEX alpha_value_idx", 556 "CREATE TABLE extra (value INTEGER)", 557 "DROP INDEX alpha_value_idx; CREATE INDEX alpha_value_idx ON alpha(value DESC)", 558 "DROP TRIGGER alpha_guard; CREATE TRIGGER alpha_guard BEFORE UPDATE ON alpha BEGIN SELECT RAISE(ABORT, 'changed'); END", 559 "ALTER TABLE alpha ADD COLUMN changed TEXT", 560 "CREATE VIEW alpha_view AS SELECT value FROM alpha", 561 ]; 562 for mutation in cases { 563 let mut connection = full_database().await; 564 sqlx::raw_sql(mutation) 565 .execute(&mut connection) 566 .await 567 .unwrap(); 568 let error = verify_schema_catalog(&mut connection, &expected(full_objects()), 1) 569 .await 570 .expect_err("schema drift must fail"); 571 assert_eq!(error.kind(), ServiceSqliteErrorKind::Integrity); 572 assert!(!error.to_string().contains("alpha")); 573 assert!(!format!("{error:?}").contains("alpha")); 574 } 575 576 let mut missing = shared_database().await; 577 assert_eq!( 578 verify_schema_catalog(&mut missing, &expected(full_objects()), 1) 579 .await 580 .expect_err("missing table") 581 .kind(), 582 ServiceSqliteErrorKind::Integrity 583 ); 584 } 585 586 #[tokio::test(flavor = "current_thread")] 587 async fn oversized_persisted_sql_is_rejected_before_rust_decode() { 588 let mut connection = shared_database().await; 589 let oversized = "x".repeat(catalog::MAX_SCHEMA_SQL_UTF8_BYTES + 1); 590 let statement = format!("CREATE TABLE oversized (value TEXT DEFAULT '{oversized}')"); 591 sqlx::query(sqlx::AssertSqlSafe(statement)) 592 .execute(&mut connection) 593 .await 594 .unwrap(); 595 let error = verify_schema_catalog(&mut connection, &expected(Vec::new()), 1) 596 .await 597 .expect_err("oversized SQL must fail"); 598 assert_eq!(error.kind(), ServiceSqliteErrorKind::Integrity); 599 assert!(!error.to_string().contains(&oversized)); 600 assert!(!format!("{error:?}").contains(&oversized)); 601 } 602 603 #[test] 604 fn runtime_names_bind_every_grammar_constraint() { 605 assert!(runtime_name_is_valid("a")); 606 assert!(runtime_name_is_valid("alpha_2")); 607 assert!(!runtime_name_is_valid("")); 608 assert!(!runtime_name_is_valid("Alpha")); 609 assert!(!runtime_name_is_valid("2alpha")); 610 assert!(!runtime_name_is_valid("alpha_")); 611 assert!(!runtime_name_is_valid("alpha__beta")); 612 assert!(!runtime_name_is_valid("alpha-beta")); 613 assert!(runtime_name_is_valid(&"a".repeat(128))); 614 assert!(!runtime_name_is_valid(&"a".repeat(129))); 615 } 616 }