v0.3.3-pre.003-fix.001
This commit is contained in:
@@ -1,89 +1,103 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
|
||||
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
|
||||
}
|
||||
|
||||
fn embedded(version: i64, name: &'static str, sql: &'static str) -> super::EmbeddedMigration {
|
||||
return super::EmbeddedMigration { hook: super::MigrationHook::None, name, sql, version };
|
||||
fn embedded(version: i64, name: &'static str, resources: &'static [crate::SchemaResource]) -> super::EmbeddedMigration {
|
||||
return super::EmbeddedMigration {
|
||||
checksum: super::MigrationChecksum::Resources,
|
||||
hook: super::MigrationHook::None,
|
||||
name,
|
||||
resources,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_embedded_registry_keeps_v000_immutable_and_adds_exact_v001() {
|
||||
fn pre_003_fix_001_embedded_registry_keeps_v000_checksum_and_uses_resource_owned_v001() {
|
||||
assert_eq!(super::EMBEDDED_MIGRATIONS.len(), 2);
|
||||
let v000 = &super::EMBEDDED_MIGRATIONS[0];
|
||||
assert_eq!(v000.version, 0);
|
||||
assert_eq!(v000.name, "bootstrap");
|
||||
assert_eq!(v000.hook, super::MigrationHook::None);
|
||||
assert!(v000.sql.contains("CREATE TABLE ksp_store_schema_migrations"));
|
||||
for forbidden in ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED"] {
|
||||
assert!(!v000.sql.contains(forbidden), "business schema leaked into immutable V000 SQL: {forbidden}");
|
||||
}
|
||||
assert_eq!(super::migration_checksum(v000.sql), "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
|
||||
assert_eq!(v000.resources.len(), 1);
|
||||
assert!(v000.resources[0].sql.contains("CREATE TABLE ksp_store_schema_migrations"));
|
||||
assert_eq!(super::migration_checksum(v000), "d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450");
|
||||
let v001 = &super::EMBEDDED_MIGRATIONS[1];
|
||||
assert_eq!(v001.version, 1);
|
||||
assert_eq!(v001.name, "raw_transaction");
|
||||
assert_eq!(v001.hook, super::MigrationHook::StoreIdentity);
|
||||
assert_eq!(super::migration_checksum(v001.sql), "6fe57ed0313d2ed295280dd6e49f6d86695d4e4effee2724a25e36db1ea17761");
|
||||
assert_eq!(v001.resources.len(), 40);
|
||||
assert_eq!(super::migration_checksum(v001), "31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51");
|
||||
assert!(super::validate_embedded_registry(super::EMBEDDED_MIGRATIONS).is_ok());
|
||||
assert_eq!(crate::current_migration_version(), 1);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_v001_inventory_indexes_and_api_bounds_are_exact() {
|
||||
let sql = super::EMBEDDED_MIGRATIONS[1].sql;
|
||||
fn pre_003_fix_001_v001_resource_order_and_api_bounds_are_exact() {
|
||||
let v001 = &super::EMBEDDED_MIGRATIONS[1];
|
||||
let ids = v001.resources.iter().map(|resource| return resource.id).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(ids.iter().filter(|id| return id.starts_with("tables/")).count(), 4);
|
||||
assert_eq!(ids.iter().filter(|id| return id.starts_with("constraints/")).count(), 35);
|
||||
assert_eq!(ids.iter().filter(|id| return id.starts_with("indexes/")).count(), 1);
|
||||
assert!(ids[..4].iter().all(|id| return id.starts_with("tables/")));
|
||||
assert!(ids[4..39].iter().all(|id| return id.starts_with("constraints/")));
|
||||
assert!(ids[39..].iter().all(|id| return id.starts_with("indexes/")));
|
||||
let mut unique = std::collections::BTreeSet::<&str>::new();
|
||||
for id in &ids {
|
||||
assert!(unique.insert(id), "duplicate embedded migration resource: {id}");
|
||||
}
|
||||
let sql = v001.resources.iter().map(|resource| return resource.sql).collect::<std::vec::Vec<_>>().concat();
|
||||
for required in [
|
||||
"CREATE TABLE ksp_store_identity",
|
||||
"CREATE TABLE ksp_raw_transactions",
|
||||
"CREATE TABLE ksp_raw_transaction_observations",
|
||||
"CREATE TABLE ksp_raw_transaction_archive_payloads",
|
||||
"CREATE INDEX ix_ksp_raw_transactions_slot_signature",
|
||||
"CREATE TABLE IF NOT EXISTS ksp_store_identity",
|
||||
"CREATE TABLE IF NOT EXISTS ksp_raw_transactions",
|
||||
"CREATE TABLE IF NOT EXISTS ksp_raw_transaction_observations",
|
||||
"CREATE TABLE IF NOT EXISTS ksp_raw_transaction_archive_payloads",
|
||||
"CREATE INDEX IF NOT EXISTS ix_ksp_raw_transactions_slot_signature",
|
||||
"WHERE retention_state <> 'purged'",
|
||||
"octet_length(signature) = 64",
|
||||
"slot BETWEEN 0 AND 18446744073709551615",
|
||||
"block_time_unix_millis BETWEEN 0 AND 253402300799999",
|
||||
"slot >= 0 AND slot <= 18446744073709551615",
|
||||
"block_time_unix_millis >= 0 AND block_time_unix_millis <= 253402300799999",
|
||||
"octet_length(content_hash) = 32",
|
||||
"octet_length(payload) BETWEEN 1 AND 16777216",
|
||||
"format_version BETWEEN 1 AND 4294967295",
|
||||
"received_at_unix_millis BETWEEN 0 AND 253402300799999",
|
||||
"source_payload_size_bytes BETWEEN 0 AND 67108864",
|
||||
"origin IN ('backfill', 'import', 'live', 'repair', 'replay')",
|
||||
"retention_state IN ('full', 'archived', 'purged')",
|
||||
"octet_length(payload) >= 1 AND octet_length(payload) <= 16777216",
|
||||
"format_version >= 1 AND format_version <= 4294967295",
|
||||
"received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999",
|
||||
"source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864",
|
||||
"origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay'",
|
||||
"retention_state = 'full' OR retention_state = 'archived' OR retention_state = 'purged'",
|
||||
] {
|
||||
assert!(sql.contains(required), "V001 physical contract is missing: {required}");
|
||||
}
|
||||
assert_eq!(sql.matches("PRIMARY KEY").count(), 4);
|
||||
assert_eq!(sql.matches("REFERENCES ksp_raw_transactions(signature) ON DELETE RESTRICT").count(), 2);
|
||||
assert!(!sql.contains("compacted"));
|
||||
assert!(!sql.contains("BIGSERIAL"));
|
||||
assert!(!sql.contains("slot BIGINT"));
|
||||
assert!(sql.contains("network TEXT NOT NULL"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_ordered_registry_accepts_v000_prefix_and_full_v001_history() {
|
||||
fn pre_003_fix_001_ordered_registry_accepts_v000_prefix_and_full_v001_history() {
|
||||
let v000 = super::EMBEDDED_MIGRATIONS[0];
|
||||
let v001 = super::EMBEDDED_MIGRATIONS[1];
|
||||
let v000_checksum = super::migration_checksum(v000.sql);
|
||||
let v000_checksum = super::migration_checksum(&v000);
|
||||
let prefix = [applied(0, v000.name, v000_checksum.as_str())];
|
||||
assert_eq!(super::validate_history(&prefix, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(1));
|
||||
let v001_checksum = super::migration_checksum(v001.sql);
|
||||
let v001_checksum = super::migration_checksum(&v001);
|
||||
let full = [applied(0, v000.name, v000_checksum.as_str()), applied(1, v001.name, v001_checksum.as_str())];
|
||||
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(2));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_registry_rejects_empty_nonzero_gap_and_empty_metadata_entries() {
|
||||
fn pre_003_fix_001_registry_rejects_empty_nonzero_gap_empty_metadata_and_empty_resources() {
|
||||
let empty: [super::EmbeddedMigration; 0] = [];
|
||||
let starts_at_one = [embedded(1, "future", "SELECT 1;")];
|
||||
let gap = [embedded(0, "bootstrap", "SELECT 0;"), embedded(2, "future", "SELECT 2;")];
|
||||
let empty_name = [embedded(0, "", "SELECT 0;")];
|
||||
let empty_sql = [embedded(0, "bootstrap", "")];
|
||||
for registry in [&empty[..], &starts_at_one[..], &gap[..], &empty_name[..], &empty_sql[..]] {
|
||||
let starts_at_one = [embedded(1, "future", crate::V000_RESOURCES)];
|
||||
let gap = [embedded(0, "bootstrap", crate::V000_RESOURCES), embedded(2, "future", crate::V000_RESOURCES)];
|
||||
let empty_name = [embedded(0, "", crate::V000_RESOURCES)];
|
||||
let empty_resources = [embedded(0, "bootstrap", &[])];
|
||||
for registry in [&empty[..], &starts_at_one[..], &gap[..], &empty_name[..], &empty_resources[..]] {
|
||||
let result = super::validate_embedded_registry(registry);
|
||||
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
|
||||
}
|
||||
@@ -91,11 +105,11 @@ fn pre_002_registry_rejects_empty_nonzero_gap_and_empty_metadata_entries() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_divergent_missing_or_gapped_history_is_terminal_mismatch() {
|
||||
fn pre_003_fix_001_divergent_missing_or_gapped_history_is_terminal_mismatch() {
|
||||
let v000 = super::EMBEDDED_MIGRATIONS[0];
|
||||
let v001 = super::EMBEDDED_MIGRATIONS[1];
|
||||
let v000_checksum = super::migration_checksum(v000.sql);
|
||||
let v001_checksum = super::migration_checksum(v001.sql);
|
||||
let v000_checksum = super::migration_checksum(&v000);
|
||||
let v001_checksum = super::migration_checksum(&v001);
|
||||
let wrong_name = [applied(0, "changed", v000_checksum.as_str())];
|
||||
let wrong_checksum = [applied(0, v000.name, "00")];
|
||||
let missing: [super::AppliedMigration; 0] = [];
|
||||
@@ -108,16 +122,12 @@ fn pre_003_divergent_missing_or_gapped_history_is_terminal_mismatch() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_newer_history_is_rejected_without_down_migration() {
|
||||
fn pre_003_fix_001_newer_history_is_rejected_without_down_migration() {
|
||||
let v000 = super::EMBEDDED_MIGRATIONS[0];
|
||||
let v001 = super::EMBEDDED_MIGRATIONS[1];
|
||||
let v000_checksum = super::migration_checksum(v000.sql);
|
||||
let v001_checksum = super::migration_checksum(v001.sql);
|
||||
let history = [
|
||||
applied(0, v000.name, v000_checksum.as_str()),
|
||||
applied(1, v001.name, v001_checksum.as_str()),
|
||||
applied(2, "future", "future-checksum"),
|
||||
];
|
||||
let v000_checksum = super::migration_checksum(&v000);
|
||||
let v001_checksum = super::migration_checksum(&v001);
|
||||
let history = [applied(0, v000.name, v000_checksum.as_str()), applied(1, v001.name, v001_checksum.as_str()), applied(2, "future", "future-checksum")];
|
||||
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS);
|
||||
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
|
||||
return;
|
||||
|
||||
114
crates/ksp-store-postgres-lib/unit_tests/schema.rs
Normal file
114
crates/ksp-store-postgres-lib/unit_tests/schema.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/schema.rs
|
||||
// version: 1
|
||||
|
||||
fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn {
|
||||
return super::ActualColumn {
|
||||
default: std::option::Option::None,
|
||||
generated: "NEVER".to_owned(),
|
||||
identity: "NO".to_owned(),
|
||||
name: name.to_owned(),
|
||||
nullable,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: udt_name.to_owned(),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_v000_resource_is_relocated_without_changing_legacy_sql() {
|
||||
assert_eq!(crate::V000_RESOURCES.len(), 1);
|
||||
let resource = crate::V000_RESOURCES[0];
|
||||
assert_eq!(resource.id, "tables/001_ksp_store_schema_migrations.sql");
|
||||
assert_eq!(
|
||||
resource.sql,
|
||||
"CREATE TABLE ksp_store_schema_migrations (\n version BIGINT PRIMARY KEY,\n name TEXT NOT NULL,\n checksum TEXT NOT NULL,\n applied_at TIMESTAMPTZ NOT NULL\n);\n",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_v001_resources_are_split_and_idempotent_by_object_family() {
|
||||
assert_eq!(crate::V001_RESOURCES.len(), 40);
|
||||
let table_resources = crate::V001_RESOURCES.iter().filter(|resource| return resource.id.starts_with("tables/")).collect::<std::vec::Vec<_>>();
|
||||
let constraint_resources = crate::V001_RESOURCES.iter().filter(|resource| return resource.id.starts_with("constraints/")).collect::<std::vec::Vec<_>>();
|
||||
let index_resources = crate::V001_RESOURCES.iter().filter(|resource| return resource.id.starts_with("indexes/")).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(table_resources.len(), 4);
|
||||
assert_eq!(constraint_resources.len(), 35);
|
||||
assert_eq!(index_resources.len(), 1);
|
||||
for resource in table_resources {
|
||||
assert!(resource.sql.contains("CREATE TABLE IF NOT EXISTS"));
|
||||
assert!(resource.sql.contains("ADD COLUMN IF NOT EXISTS"));
|
||||
}
|
||||
for resource in constraint_resources {
|
||||
assert!(resource.sql.contains("IF NOT EXISTS"));
|
||||
}
|
||||
for resource in index_resources {
|
||||
assert!(resource.sql.contains("CREATE INDEX IF NOT EXISTS"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_external_extra_columns_are_accepted_only_when_they_cannot_block_ksp_inserts() {
|
||||
let nullable = actual_column("external_nullable", "text", true);
|
||||
assert!(nullable.is_non_blocking_extra());
|
||||
let mut with_default = actual_column("external_default", "text", true);
|
||||
with_default.default = std::option::Option::Some("'x'::text".to_owned());
|
||||
assert!(!with_default.is_non_blocking_extra());
|
||||
let mut identity = actual_column("external_identity", "int8", true);
|
||||
identity.identity = "YES".to_owned();
|
||||
assert!(!identity.is_non_blocking_extra());
|
||||
let mut generated = actual_column("external_generated", "text", true);
|
||||
generated.generated = "ALWAYS".to_owned();
|
||||
assert!(!generated.is_non_blocking_extra());
|
||||
let blocking = actual_column("external_required", "text", false);
|
||||
assert!(!blocking.is_non_blocking_extra());
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_required_column_matching_ignores_integer_precision_but_requires_numeric_20_0() {
|
||||
let expected_int = super::ColumnContract {
|
||||
name: "version",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::None,
|
||||
numeric_scale: std::option::Option::None,
|
||||
udt_name: "int8",
|
||||
};
|
||||
let mut actual_int = actual_column("version", "int8", false);
|
||||
actual_int.numeric_precision = std::option::Option::Some(64);
|
||||
actual_int.numeric_scale = std::option::Option::Some(0);
|
||||
assert!(actual_int.matches(&expected_int));
|
||||
let expected_numeric = super::ColumnContract {
|
||||
name: "slot",
|
||||
nullable: false,
|
||||
numeric_precision: std::option::Option::Some(20),
|
||||
numeric_scale: std::option::Option::Some(0),
|
||||
udt_name: "numeric",
|
||||
};
|
||||
let mut actual_numeric = actual_column("slot", "numeric", false);
|
||||
actual_numeric.numeric_precision = std::option::Option::Some(20);
|
||||
actual_numeric.numeric_scale = std::option::Option::Some(0);
|
||||
assert!(actual_numeric.matches(&expected_numeric));
|
||||
actual_numeric.numeric_precision = std::option::Option::Some(19);
|
||||
assert!(!actual_numeric.matches(&expected_numeric));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_fix_001_catalog_normalization_and_resource_owned_constraint_definition_are_deterministic() {
|
||||
let normalized = super::normalize_catalog_sql("CHECK ((slot >= (0)::numeric) AND (slot <= (18446744073709551615)::numeric))");
|
||||
assert_eq!(normalized, "checkslot>=0andslot<=18446744073709551615");
|
||||
let resource = crate::V001_RESOURCES.iter().find(|resource| return resource.id == "constraints/006_ck_ksp_raw_transactions_slot.sql");
|
||||
let resource = match resource {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
assert!(false, "V001 slot constraint resource must remain embedded");
|
||||
return;
|
||||
},
|
||||
};
|
||||
let expected = super::expected_constraint_definition(resource.sql, "ck_ksp_raw_transactions_slot");
|
||||
assert_eq!(expected.as_deref(), std::option::Option::Some(normalized.as_str()));
|
||||
assert_ne!(expected.as_deref(), std::option::Option::Some("checkslot>=0andslot<=10"));
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user