v0.3.3-pre.003-fix.001

This commit is contained in:
2026-08-30 10:21:48 +02:00
parent 61bf7ba468
commit c17e78c6a8
64 changed files with 2890 additions and 424 deletions

View File

@@ -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;

View 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;
}