224 lines
12 KiB
Rust
224 lines
12 KiB
Rust
// file: crates/ksp-store-postgres-lib/unit_tests/schema.rs
|
|
// version: 7
|
|
|
|
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_v002_resources_are_complete_bounded_and_have_one_unfiltered_navigation_index() {
|
|
assert_eq!(crate::V002_RESOURCES.len(), 32);
|
|
let ids = crate::V002_RESOURCES.iter().map(|resource| return resource.id).collect::<std::vec::Vec<_>>();
|
|
assert_eq!(ids.iter().filter(|id| return id.starts_with("tables/")).count(), 2);
|
|
assert_eq!(ids.iter().filter(|id| return id.starts_with("constraints/")).count(), 29);
|
|
assert_eq!(ids.iter().filter(|id| return id.starts_with("indexes/")).count(), 1);
|
|
assert!(ids[..2].iter().all(|id| return id.starts_with("tables/")));
|
|
assert!(ids[2..31].iter().all(|id| return id.starts_with("constraints/")));
|
|
assert_eq!(ids[31], "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
|
|
let mut unique = std::collections::BTreeSet::<&str>::new();
|
|
for id in &ids {
|
|
assert!(unique.insert(id), "duplicate V002 resource: {id}");
|
|
}
|
|
let sql = crate::V002_RESOURCES.iter().map(|resource| return resource.sql).collect::<std::vec::Vec<_>>().concat();
|
|
let normalized_sql = sql.split_ascii_whitespace().collect::<std::vec::Vec<_>>().join(" ");
|
|
for required in [
|
|
"CREATE TABLE IF NOT EXISTS ksp_raw_account_states",
|
|
"CREATE TABLE IF NOT EXISTS ksp_raw_account_observations",
|
|
"slot NUMERIC(20, 0) NOT NULL",
|
|
"lamports NUMERIC(20, 0) NOT NULL",
|
|
"rent_epoch NUMERIC(20, 0) NOT NULL",
|
|
"write_version NUMERIC(20, 0) NULL",
|
|
"PRIMARY KEY (pubkey, slot, state_hash)",
|
|
"PRIMARY KEY (observation_key)",
|
|
"FOREIGN KEY (account_pubkey, account_slot, account_state_hash) REFERENCES ksp_raw_account_states(pubkey, slot, state_hash) ON DELETE RESTRICT",
|
|
"octet_length(pubkey) = 32",
|
|
"slot >= 0 AND slot <= 18446744073709551615",
|
|
"octet_length(state_hash) = 32",
|
|
"lamports >= 0 AND lamports <= 18446744073709551615",
|
|
"octet_length(owner) = 32",
|
|
"rent_epoch >= 0 AND rent_epoch <= 18446744073709551615",
|
|
"octet_length(data) <= 16777216",
|
|
"octet_length(observation_key) = 32",
|
|
"octet_length(account_pubkey) = 32",
|
|
"account_slot >= 0 AND account_slot <= 18446744073709551615",
|
|
"octet_length(account_state_hash) = 32",
|
|
"octet_length(provider) >= 1 AND octet_length(provider) <= 128",
|
|
"octet_length(protocol) >= 1 AND octet_length(protocol) <= 128",
|
|
"octet_length(acquisition_method) >= 1 AND octet_length(acquisition_method) <= 128",
|
|
"origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay'",
|
|
"received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999",
|
|
"observed_at_unix_millis IS NULL OR observed_at_unix_millis <= received_at_unix_millis",
|
|
"source_payload_size_bytes IS NULL OR source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864",
|
|
"transaction_signature IS NULL OR octet_length(transaction_signature) = 64",
|
|
"write_version IS NULL OR write_version >= 0 AND write_version <= 18446744073709551615",
|
|
"CREATE INDEX IF NOT EXISTS ix_ksp_raw_account_states_slot_pubkey_state_hash ON ksp_raw_account_states (slot, pubkey, state_hash)",
|
|
] {
|
|
assert!(normalized_sql.contains(required), "V002 final physical contract is missing: {required}");
|
|
}
|
|
for forbidden in [
|
|
"ON DELETE CASCADE",
|
|
"BIGSERIAL",
|
|
"slot BIGINT",
|
|
"lamports BIGINT",
|
|
"rent_epoch BIGINT",
|
|
"write_version BIGINT",
|
|
"data TEXT",
|
|
"CREATE UNIQUE INDEX",
|
|
] {
|
|
assert!(!normalized_sql.contains(forbidden), "V002 contains forbidden physical policy/index shape: {forbidden}");
|
|
}
|
|
let index_resource = &crate::V002_RESOURCES[31];
|
|
let contract = match index_resource.object {
|
|
super::SchemaObjectContract::Index(value) => value,
|
|
super::SchemaObjectContract::Constraint(_) | super::SchemaObjectContract::Table(_) => panic!("V002 final resource must be the navigation index"),
|
|
};
|
|
assert_eq!(contract.key_fragment, "slot,pubkey,state_hash");
|
|
assert_eq!(contract.predicate_fragment, std::option::Option::None);
|
|
assert!(!contract.unique);
|
|
assert!(!index_resource.sql.contains("WHERE"));
|
|
return;
|
|
}
|
|
|
|
#[test]
|
|
fn pre_003_v002_external_compatibility_recognizes_only_owned_constraints() {
|
|
assert!(super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_states", "ck_ksp_raw_account_states_slot"));
|
|
assert!(super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_observations", "fk_ksp_raw_account_observations_state",));
|
|
assert!(!super::is_expected_constraint(crate::V002_RESOURCES, "ksp_raw_account_states", "external_check"));
|
|
let slot_resource = crate::V002_RESOURCES.iter().find(|resource| return resource.id == "constraints/005_ck_ksp_raw_account_states_slot.sql");
|
|
let slot_resource = match slot_resource {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("V002 slot constraint must remain embedded"),
|
|
};
|
|
let expected = super::expected_constraint_definition(slot_resource.sql, "ck_ksp_raw_account_states_slot");
|
|
let expected = match expected {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("V002 slot constraint definition must be extractable"),
|
|
};
|
|
assert!(super::matches_expected_constraint_definition(crate::V002_RESOURCES, "ksp_raw_account_states", "c", expected.as_str()));
|
|
assert!(!super::matches_expected_constraint_definition(crate::V001_RESOURCES, "ksp_raw_account_states", "c", expected.as_str()));
|
|
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 postgres_17_normalized = super::normalize_catalog_sql("CHECK ((slot >= '0'::numeric) AND (slot <= '18446744073709551615'::numeric))");
|
|
assert_eq!(postgres_17_normalized, normalized);
|
|
let bigint_normalized = super::normalize_catalog_sql(
|
|
"CHECK ((block_time_unix_millis IS NULL) OR ((block_time_unix_millis >= '0'::bigint) AND (block_time_unix_millis <= '253402300799999'::bigint)))",
|
|
);
|
|
let block_time_resource = crate::V001_RESOURCES.iter().find(|resource| return resource.id == "constraints/007_ck_ksp_raw_transactions_block_time.sql");
|
|
assert!(block_time_resource.is_some(), "V001 block-time constraint resource must remain embedded");
|
|
let block_time_resource = match block_time_resource {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return,
|
|
};
|
|
let block_time_expected = super::expected_constraint_definition(block_time_resource.sql, "ck_ksp_raw_transactions_block_time");
|
|
assert_eq!(block_time_expected.as_deref(), std::option::Option::Some(bigint_normalized.as_str()));
|
|
let integer_alias = super::normalize_catalog_sql("CHECK (format_version <= '4294967295'::int8)");
|
|
assert_eq!(integer_alias, "checkformat_version<=4294967295");
|
|
let text_literal = super::normalize_catalog_sql("CHECK (retention_state = 'full'::text)");
|
|
assert_eq!(text_literal, "checkretention_state='full'");
|
|
let resource = crate::V001_RESOURCES.iter().find(|resource| return resource.id == "constraints/006_ck_ksp_raw_transactions_slot.sql");
|
|
assert!(resource.is_some(), "V001 slot constraint resource must remain embedded");
|
|
let resource = match resource {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => 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;
|
|
}
|