v0.3.4-pre.003

This commit is contained in:
2026-08-30 21:37:02 +02:00
parent dfa3723a7f
commit a704a5722e
37 changed files with 1088 additions and 75 deletions

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_pubkey'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_pubkey CHECK (octet_length(pubkey) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_slot'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_slot CHECK (slot >= 0 AND slot <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_state_hash'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_state_hash CHECK (octet_length(state_hash) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_lamports'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_lamports CHECK (lamports >= 0 AND lamports <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_owner'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_owner CHECK (octet_length(owner) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_rent_epoch'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_rent_epoch CHECK (rent_epoch >= 0 AND rent_epoch <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_states_data'
AND conrelid = to_regclass('ksp_raw_account_states')
) THEN
ALTER TABLE ksp_raw_account_states ADD CONSTRAINT ck_ksp_raw_account_states_data CHECK (octet_length(data) <= 16777216);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_key'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_key CHECK (octet_length(observation_key) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_account_pubkey'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_pubkey CHECK (octet_length(account_pubkey) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_account_slot'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_slot CHECK (account_slot >= 0 AND account_slot <= 18446744073709551615);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_account_state_hash'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_account_state_hash CHECK (octet_length(account_state_hash) = 32);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_provider'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_provider CHECK (
octet_length(provider) >= 1 AND octet_length(provider) <= 128
AND provider ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_protocol'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_protocol CHECK (
octet_length(protocol) >= 1 AND octet_length(protocol) <= 128
AND protocol ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_method'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_method CHECK (
octet_length(acquisition_method) >= 1 AND octet_length(acquisition_method) <= 128
AND acquisition_method ~ '^[A-Za-z0-9_.:-]+$'
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_origin'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_origin CHECK (origin = 'backfill' OR origin = 'import' OR origin = 'live' OR origin = 'repair' OR origin = 'replay');
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_received_at'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_received_at CHECK (received_at_unix_millis >= 0 AND received_at_unix_millis <= 253402300799999);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_capture_session'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_capture_session CHECK (
capture_session_id IS NULL
OR (
octet_length(capture_session_id) >= 1 AND octet_length(capture_session_id) <= 128
AND capture_session_id ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_commitment'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_commitment CHECK (
commitment IS NULL
OR (
octet_length(commitment) >= 1 AND octet_length(commitment) <= 128
AND commitment ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_endpoint'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_endpoint CHECK (
endpoint_id IS NULL
OR (
octet_length(endpoint_id) >= 1 AND octet_length(endpoint_id) <= 128
AND endpoint_id ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,18 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_filter'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_filter CHECK (
filter_id IS NULL
OR (
octet_length(filter_id) >= 1 AND octet_length(filter_id) <= 128
AND filter_id ~ '^[A-Za-z0-9_.:-]+$'
)
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_observed_at'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_observed_at CHECK (
observed_at_unix_millis IS NULL
OR observed_at_unix_millis >= 0 AND observed_at_unix_millis <= 253402300799999
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,12 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_time_order'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_time_order CHECK (observed_at_unix_millis IS NULL OR observed_at_unix_millis <= received_at_unix_millis);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_source_hash'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_source_hash CHECK (
source_payload_hash IS NULL
OR octet_length(source_payload_hash) = 32
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_source_size'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_source_size CHECK (
source_payload_size_bytes IS NULL
OR source_payload_size_bytes >= 0 AND source_payload_size_bytes <= 67108864
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_transaction_signature'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_transaction_signature CHECK (
transaction_signature IS NULL
OR octet_length(transaction_signature) = 64
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,15 @@
DO $ksp$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_ksp_raw_account_observations_write_version'
AND conrelid = to_regclass('ksp_raw_account_observations')
) THEN
ALTER TABLE ksp_raw_account_observations ADD CONSTRAINT ck_ksp_raw_account_observations_write_version CHECK (
write_version IS NULL
OR write_version >= 0 AND write_version <= 18446744073709551615
);
END IF;
END
$ksp$;

View File

@@ -0,0 +1,2 @@
CREATE INDEX IF NOT EXISTS ix_ksp_raw_account_states_slot_pubkey_state_hash
ON ksp_raw_account_states (slot, pubkey, state_hash);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 15
// version: 16
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -23,6 +23,9 @@
//! `PostgresBackend` while preserving the existing narrow backend bridge.
//! `0.3.4-pre.002` registers additive V002 and its two minimal RAW account
//! tables with canonical state/observation PKs and the observation-state FK.
//! `0.3.4-pre.003` completes V002 with exact physical bounds, one unfiltered
//! navigation index, external-schema compatibility and the bounded prerelease
//! checksum transition from the provisional `pre.002` schema.
//!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! common facade consumes only this crate's narrow backend bridge and never
@@ -99,9 +102,11 @@ pub(crate) use self::schema::V001_RESOURCES;
pub(crate) use self::schema::V002_RESOURCES;
/// Private physical schema resource inspector consumed by the migration engine.
pub(crate) use self::schema::inspect_resource;
/// Private V001 adoption probe consumed by the migration engine.
pub(crate) use self::schema::managed_v001_objects_exist;
/// Private external-schema compatibility gate consumed by the migration engine.
/// Private managed-schema adoption probe consumed by the migration engine.
pub(crate) use self::schema::managed_schema_objects_exist;
/// Private V001 external-schema compatibility gate consumed by the migration engine.
pub(crate) use self::schema::verify_v001_external_compatibility;
/// Private V002 external-schema compatibility gate consumed by the migration engine.
pub(crate) use self::schema::verify_v002_external_compatibility;
const _: &str = crate::TRACING_TARGET;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/migration.rs
// version: 8
// version: 9
use sha2::Digest; // rust-rules: trait-import
@@ -9,6 +9,7 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
checksum: MigrationChecksum::LegacySql(include_str!("../migrations/v000_bootstrap/tables/001_ksp_store_schema_migrations.sql")),
hook: MigrationHook::None,
name: "bootstrap",
previous_checksums: &[],
resources: crate::V000_RESOURCES,
version: 0,
},
@@ -16,6 +17,7 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
checksum: MigrationChecksum::Resources,
hook: MigrationHook::StoreIdentity,
name: "raw_transaction",
previous_checksums: &[],
resources: crate::V001_RESOURCES,
version: 1,
},
@@ -23,12 +25,14 @@ const EMBEDDED_MIGRATIONS: &[EmbeddedMigration] = &[
checksum: MigrationChecksum::Resources,
hook: MigrationHook::None,
name: "raw_account_state",
previous_checksums: &[V002_PROVISIONAL_CHECKSUM_PRE_002],
resources: crate::V002_RESOURCES,
version: 2,
},
];
const HEX_LOWER: &[u8; 16] = b"0123456789abcdef";
const HISTORY_INSERT_SQL: &str = "INSERT INTO ksp_store_schema_migrations (version, name, checksum, applied_at) VALUES ($1, $2, $3, CURRENT_TIMESTAMP)";
const HISTORY_UPDATE_CHECKSUM_SQL: &str = "UPDATE ksp_store_schema_migrations SET checksum = $1 WHERE version = $2 AND name = $3 AND checksum = $4";
const HISTORY_LOAD_SQL: &str = "SELECT version, name, checksum FROM ksp_store_schema_migrations ORDER BY version";
const IDENTITY_INSERT_SQL: &str = "INSERT INTO ksp_store_identity (singleton, network) VALUES (1, $1)";
const IDENTITY_LOAD_SQL: &str = "SELECT singleton, network FROM ksp_store_identity ORDER BY singleton LIMIT 2";
@@ -40,6 +44,7 @@ const METADATA_EXISTS_SQL: &str = r#"SELECT EXISTS (
AND table_type = 'BASE TABLE'
)"#;
const SET_STATEMENT_TIMEOUT_SQL: &str = "SELECT set_config('statement_timeout', $1, true)";
const V002_PROVISIONAL_CHECKSUM_PRE_002: &str = "30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55";
struct AppliedMigration {
checksum: std::string::String,
@@ -58,6 +63,7 @@ struct EmbeddedMigration {
checksum: MigrationChecksum,
hook: MigrationHook,
name: &'static str,
previous_checksums: &'static [&'static str],
resources: &'static [crate::SchemaResource],
version: i64,
}
@@ -140,7 +146,7 @@ async fn bootstrap_inner(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (next_index, mutation_mode) = if metadata_exists {
let (next_index, mutation_mode, applied_history) = if metadata_exists {
let metadata_result = crate::inspect_resource(&transaction, &crate::V000_RESOURCES[0]).await;
match metadata_result {
std::result::Result::Ok(crate::SchemaResourceState::Compatible) => {},
@@ -155,18 +161,18 @@ async fn bootstrap_inner(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS);
let validation_result = validate_history(history.as_slice(), EMBEDDED_MIGRATIONS, schema_autoupdate);
let index = match validation_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
(index, SchemaMutationMode::Update)
(index, SchemaMutationMode::Update, std::option::Option::Some(history))
} else {
if !schema_autocreate {
log_schema_block("schema_autocreate_disabled");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_autocreate_disabled"));
}
let managed_result = crate::managed_v001_objects_exist(&transaction).await;
let managed_result = crate::managed_schema_objects_exist(&transaction).await;
let managed_objects_exist = match managed_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -175,12 +181,18 @@ async fn bootstrap_inner(
log_schema_block("schema_adoption_disabled");
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "schema_adoption_disabled"));
}
(0, SchemaMutationMode::Create)
(0, SchemaMutationMode::Create, std::option::Option::None)
};
let existing_schema_result = verify_or_repair_applied_migrations(&transaction, next_index, schema_autoupdate).await;
if let std::result::Result::Err(error) = existing_schema_result {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(history) = applied_history.as_deref() {
let checksum_result = reconcile_applied_history_checksums(&transaction, history, next_index, schema_autoupdate).await;
if let std::result::Result::Err(error) = checksum_result {
return std::result::Result::Err(error);
}
}
let existing_hook_result = run_applied_migration_hooks(&transaction, network, next_index).await;
if let std::result::Result::Err(error) = existing_hook_result {
return std::result::Result::Err(error);
@@ -528,12 +540,14 @@ async fn set_statement_timeout(
}
async fn verify_migration_contract(transaction: &deadpool_postgres::Transaction<'_>, version: i64) -> std::result::Result<(), crate::PostgresBackendError> {
if version == 1 {
let result = crate::verify_v001_external_compatibility(transaction).await;
if let std::result::Result::Err(error) = result {
log_schema_block(error.phase());
return std::result::Result::Err(error);
}
let result = match version {
1 => crate::verify_v001_external_compatibility(transaction).await,
2 => crate::verify_v002_external_compatibility(transaction).await,
_ => std::result::Result::Ok(()),
};
if let std::result::Result::Err(error) = result {
log_schema_block(error.phase());
return std::result::Result::Err(error);
}
return std::result::Result::Ok(());
}
@@ -582,6 +596,41 @@ async fn verify_or_repair_applied_migrations(
return std::result::Result::Ok(());
}
async fn reconcile_applied_history_checksums(
transaction: &deadpool_postgres::Transaction<'_>,
history: &[AppliedMigration],
applied_count: usize,
schema_autoupdate: bool,
) -> std::result::Result<(), crate::PostgresBackendError> {
if !schema_autoupdate {
return std::result::Result::Ok(());
}
for (index, applied) in history.iter().take(applied_count).enumerate() {
let migration = &EMBEDDED_MIGRATIONS[index];
let expected_checksum = migration_checksum(migration);
if applied.checksum == expected_checksum {
continue;
}
if !migration.previous_checksums.contains(&applied.checksum.as_str()) {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
}
let result = transaction.execute(HISTORY_UPDATE_CHECKSUM_SQL, &[&expected_checksum, &migration.version, &migration.name, &applied.checksum]).await;
match result {
std::result::Result::Ok(1) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
migration_version = migration.version,
"upgraded known prerelease PostgreSQL Store migration checksum after schema reconciliation"
);
},
std::result::Result::Ok(_) | std::result::Result::Err(_) => {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationFailed, "history_checksum_update"));
},
}
}
return std::result::Result::Ok(());
}
fn schema_autoupdate_disabled_error() -> crate::PostgresBackendError {
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "schema_autoupdate_disabled");
}
@@ -615,7 +664,11 @@ fn validate_embedded_registry(migrations: &[EmbeddedMigration]) -> std::result::
return std::result::Result::Ok(());
}
fn validate_history(history: &[AppliedMigration], migrations: &[EmbeddedMigration]) -> std::result::Result<usize, crate::PostgresBackendError> {
fn validate_history(
history: &[AppliedMigration],
migrations: &[EmbeddedMigration],
allow_previous_checksums: bool,
) -> std::result::Result<usize, crate::PostgresBackendError> {
if history.is_empty() {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_missing"));
}
@@ -630,7 +683,8 @@ fn validate_history(history: &[AppliedMigration], migrations: &[EmbeddedMigratio
}
let expected = &migrations[index];
let expected_checksum = migration_checksum(expected);
if applied.version != expected.version || applied.name != expected.name || applied.checksum != expected_checksum {
let previous_checksum_matches = allow_previous_checksums && expected.previous_checksums.contains(&applied.checksum.as_str());
if applied.version != expected.version || applied.name != expected.name || (applied.checksum != expected_checksum && !previous_checksum_matches) {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, "history_diverged"));
}
index += 1;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/schema.rs
// version: 8
// version: 9
/// Immutable V000 physical schema resource inventory.
pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource {
@@ -370,7 +370,7 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
access_method: "btree",
key_fragment: "slot,signature",
name: "ix_ksp_raw_transactions_slot_signature",
predicate_fragment: "retention_state<>'purged'",
predicate_fragment: std::option::Option::Some("retention_state<>'purged'"),
table: "ksp_raw_transactions",
unique: false,
}),
@@ -378,7 +378,7 @@ pub(crate) const V001_RESOURCES: &[SchemaResource] = &[
sql: include_str!("../migrations/v001_raw_transaction/indexes/001_ix_ksp_raw_transactions_slot_signature.sql"),
},
];
/// V002 physical schema resource inventory for the minimal RAW account state foundation.
/// Final V002 physical schema resource inventory for RAW account state persistence.
pub(crate) const V002_RESOURCES: &[SchemaResource] = &[
SchemaResource {
id: "tables/001_ksp_raw_account_states.sql",
@@ -426,6 +426,259 @@ pub(crate) const V002_RESOURCES: &[SchemaResource] = &[
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/003_fk_ksp_raw_account_observations_state.sql"),
},
SchemaResource {
id: "constraints/004_ck_ksp_raw_account_states_pubkey.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_pubkey", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql"),
},
SchemaResource {
id: "constraints/005_ck_ksp_raw_account_states_slot.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_slot", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql"),
},
SchemaResource {
id: "constraints/006_ck_ksp_raw_account_states_state_hash.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_states_state_hash",
table: "ksp_raw_account_states",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql"),
},
SchemaResource {
id: "constraints/007_ck_ksp_raw_account_states_lamports.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_lamports", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql"),
},
SchemaResource {
id: "constraints/008_ck_ksp_raw_account_states_owner.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_owner", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql"),
},
SchemaResource {
id: "constraints/009_ck_ksp_raw_account_states_rent_epoch.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_states_rent_epoch",
table: "ksp_raw_account_states",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql"),
},
SchemaResource {
id: "constraints/010_ck_ksp_raw_account_states_data.sql",
object: SchemaObjectContract::Constraint(ConstraintContract { kind: "c", name: "ck_ksp_raw_account_states_data", table: "ksp_raw_account_states" }),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql"),
},
SchemaResource {
id: "constraints/011_ck_ksp_raw_account_observations_key.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_key",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql"),
},
SchemaResource {
id: "constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_account_pubkey",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql"),
},
SchemaResource {
id: "constraints/013_ck_ksp_raw_account_observations_account_slot.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_account_slot",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql"),
},
SchemaResource {
id: "constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_account_state_hash",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql"),
},
SchemaResource {
id: "constraints/015_ck_ksp_raw_account_observations_provider.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_provider",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql"),
},
SchemaResource {
id: "constraints/016_ck_ksp_raw_account_observations_protocol.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_protocol",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql"),
},
SchemaResource {
id: "constraints/017_ck_ksp_raw_account_observations_method.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_method",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql"),
},
SchemaResource {
id: "constraints/018_ck_ksp_raw_account_observations_origin.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_origin",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql"),
},
SchemaResource {
id: "constraints/019_ck_ksp_raw_account_observations_received_at.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_received_at",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql"),
},
SchemaResource {
id: "constraints/020_ck_ksp_raw_account_observations_capture_session.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_capture_session",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql"),
},
SchemaResource {
id: "constraints/021_ck_ksp_raw_account_observations_commitment.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_commitment",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql"),
},
SchemaResource {
id: "constraints/022_ck_ksp_raw_account_observations_endpoint.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_endpoint",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql"),
},
SchemaResource {
id: "constraints/023_ck_ksp_raw_account_observations_filter.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_filter",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql"),
},
SchemaResource {
id: "constraints/024_ck_ksp_raw_account_observations_observed_at.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_observed_at",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql"),
},
SchemaResource {
id: "constraints/025_ck_ksp_raw_account_observations_time_order.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_time_order",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql"),
},
SchemaResource {
id: "constraints/026_ck_ksp_raw_account_observations_source_hash.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_source_hash",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql"),
},
SchemaResource {
id: "constraints/027_ck_ksp_raw_account_observations_source_size.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_source_size",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql"),
},
SchemaResource {
id: "constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_transaction_signature",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql"),
},
SchemaResource {
id: "constraints/029_ck_ksp_raw_account_observations_write_version.sql",
object: SchemaObjectContract::Constraint(ConstraintContract {
kind: "c",
name: "ck_ksp_raw_account_observations_write_version",
table: "ksp_raw_account_observations",
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql"),
},
SchemaResource {
id: "indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql",
object: SchemaObjectContract::Index(IndexContract {
access_method: "btree",
key_fragment: "slot,pubkey,state_hash",
name: "ix_ksp_raw_account_states_slot_pubkey_state_hash",
predicate_fragment: std::option::Option::None,
table: "ksp_raw_account_states",
unique: false,
}),
repair_existing: true,
sql: include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql"),
},
];
const COLUMN_LOAD_SQL: &str = r#"SELECT column_name::TEXT, udt_name::TEXT, (is_nullable = 'YES') AS nullable, numeric_precision::INTEGER, numeric_scale::INTEGER, column_default::TEXT, is_identity::TEXT, is_generated::TEXT
@@ -453,7 +706,7 @@ WHERE ns.nspname = current_schema() AND table_rel.relname = $1 AND index_rel.rel
const MANAGED_OBJECT_EXISTS_SQL: &str = r#"SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = current_schema()
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads')
AND table_name IN ('ksp_store_identity', 'ksp_raw_transactions', 'ksp_raw_transaction_observations', 'ksp_raw_transaction_archive_payloads', 'ksp_raw_account_states', 'ksp_raw_account_observations')
AND table_type = 'BASE TABLE'
)"#;
const PRIMARY_KEY_LOAD_SQL: &str = r#"SELECT string_agg(att.attname::TEXT, ',' ORDER BY key_part.ord)::TEXT
@@ -513,7 +766,7 @@ struct IndexContract {
access_method: &'static str,
key_fragment: &'static str,
name: &'static str,
predicate_fragment: &'static str,
predicate_fragment: std::option::Option<&'static str>,
table: &'static str,
unique: bool,
}
@@ -969,6 +1222,7 @@ const RAW_TRANSACTION_ARCHIVE_PAYLOADS_COLUMNS: &[ColumnContract] = &[
},
];
const V001_TABLE_NAMES: &[&str] = &["ksp_store_identity", "ksp_raw_transactions", "ksp_raw_transaction_observations", "ksp_raw_transaction_archive_payloads"];
const V002_TABLE_NAMES: &[&str] = &["ksp_raw_account_states", "ksp_raw_account_observations"];
/// Inspects one embedded schema resource against the effective PostgreSQL catalog.
pub(crate) async fn inspect_resource(
@@ -982,8 +1236,8 @@ pub(crate) async fn inspect_resource(
};
}
/// Returns whether any V001-managed base table already exists in the active schema.
pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
/// Returns whether any KSP-managed Store base table already exists in the active schema.
pub(crate) async fn managed_schema_objects_exist(transaction: &deadpool_postgres::Transaction<'_>) -> std::result::Result<bool, crate::PostgresBackendError> {
let result = transaction.query_one(MANAGED_OBJECT_EXISTS_SQL, &[]).await;
let row = match result {
std::result::Result::Ok(value) => value,
@@ -999,7 +1253,22 @@ pub(crate) async fn managed_v001_objects_exist(transaction: &deadpool_postgres::
pub(crate) async fn verify_v001_external_compatibility(
transaction: &deadpool_postgres::Transaction<'_>,
) -> std::result::Result<(), crate::PostgresBackendError> {
for table in V001_TABLE_NAMES {
return verify_external_compatibility(transaction, V001_TABLE_NAMES, V001_RESOURCES).await;
}
/// Rejects external V002 schema extensions that can constrain or mutate KSP writes.
pub(crate) async fn verify_v002_external_compatibility(
transaction: &deadpool_postgres::Transaction<'_>,
) -> std::result::Result<(), crate::PostgresBackendError> {
return verify_external_compatibility(transaction, V002_TABLE_NAMES, V002_RESOURCES).await;
}
async fn verify_external_compatibility(
transaction: &deadpool_postgres::Transaction<'_>,
tables: &[&str],
resources: &[SchemaResource],
) -> std::result::Result<(), crate::PostgresBackendError> {
for table in tables {
let table = *table;
let constraint_rows = transaction.query(UNEXPECTED_CONSTRAINTS_SQL, &[&table]).await;
let constraint_rows = match constraint_rows {
@@ -1024,11 +1293,11 @@ pub(crate) async fn verify_v001_external_compatibility(
) => (name, kind, validated, deferrable, deferred, definition),
_ => return schema_query_error("schema_constraint_inventory_decode"),
};
if is_expected_constraint(table, name.as_str()) {
if is_expected_constraint(resources, table, name.as_str()) {
continue;
}
let definition = normalize_catalog_sql(definition.as_str());
if !validated || deferrable || deferred || !matches_expected_constraint_definition(table, kind.as_str(), definition.as_str()) {
if !validated || deferrable || deferred || !matches_expected_constraint_definition(resources, table, kind.as_str(), definition.as_str()) {
return schema_incompatible("schema_external_constraint");
}
}
@@ -1129,9 +1398,10 @@ async fn inspect_index(
};
let definition = normalize_catalog_sql(definition.as_str());
let predicate = predicate.map(|value| return normalize_catalog_sql(value.as_str()));
let predicate_matches = match predicate.as_deref() {
std::option::Option::Some(value) => value.contains(contract.predicate_fragment),
std::option::Option::None => false,
let predicate_matches = match (contract.predicate_fragment, predicate.as_deref()) {
(std::option::Option::Some(expected), std::option::Option::Some(value)) => value.contains(expected),
(std::option::Option::None, std::option::Option::None) => true,
(std::option::Option::Some(_), std::option::Option::None) | (std::option::Option::None, std::option::Option::Some(_)) => false,
};
if unique != contract.unique || access_method != contract.access_method || !definition.contains(contract.key_fragment) || !predicate_matches {
return std::result::Result::Ok(SchemaResourceState::Incompatible);
@@ -1347,8 +1617,8 @@ fn expected_constraint_definition(resource_sql: &str, name: &str) -> std::option
return std::option::Option::Some(normalize_catalog_sql(definition));
}
fn is_expected_constraint(table: &str, name: &str) -> bool {
for resource in V001_RESOURCES {
fn is_expected_constraint(resources: &[SchemaResource], table: &str, name: &str) -> bool {
for resource in resources {
let contract = match resource.object {
SchemaObjectContract::Constraint(value) => value,
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,
@@ -1360,8 +1630,8 @@ fn is_expected_constraint(table: &str, name: &str) -> bool {
return false;
}
fn matches_expected_constraint_definition(table: &str, kind: &str, definition: &str) -> bool {
for resource in V001_RESOURCES {
fn matches_expected_constraint_definition(resources: &[SchemaResource], table: &str, kind: &str, definition: &str) -> bool {
for resource in resources {
let contract = match resource.object {
SchemaObjectContract::Constraint(value) => value,
SchemaObjectContract::Index(_) | SchemaObjectContract::Table(_) => continue,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 15
// version: 16
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -126,18 +126,28 @@ fn pre_003_fix_001_migration_engine_uses_split_schema_contract_and_binds_network
}
#[test]
fn pre_002_v002_is_migration_only_and_does_not_open_account_repository_or_runtime_dispatch() {
fn pre_003_v002_schema_is_complete_but_still_does_not_open_account_repository_or_runtime_dispatch() {
let crate_root = include_str!("../src/lib.rs");
let migration = include_str!("../src/migration.rs");
let schema = include_str!("../src/schema.rs");
let runtime = include_str!("../src/runtime.rs");
let states = include_str!("../migrations/v002_raw_account_state/tables/001_ksp_raw_account_states.sql");
let observations = include_str!("../migrations/v002_raw_account_state/tables/002_ksp_raw_account_observations.sql");
let slot_check = include_str!("../migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql");
let index = include_str!("../migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql");
assert!(migration.contains("name: \"raw_account_state\""));
assert!(migration.contains("resources: crate::V002_RESOURCES"));
assert!(migration.contains("V002_PROVISIONAL_CHECKSUM_PRE_002"));
assert!(migration.contains("HISTORY_UPDATE_CHECKSUM_SQL"));
assert!(migration.contains("reconcile_applied_history_checksums"));
assert!(schema.contains("pub(crate) const V002_RESOURCES"));
assert!(schema.contains("verify_v002_external_compatibility"));
assert!(schema.contains("ksp_raw_account_states', 'ksp_raw_account_observations"));
assert!(states.contains("ksp_raw_account_states"));
assert!(observations.contains("ksp_raw_account_observations"));
assert!(slot_check.contains("slot >= 0 AND slot <= 18446744073709551615"));
assert!(index.contains("ON ksp_raw_account_states (slot, pubkey, state_hash)"));
assert!(!index.contains("WHERE"));
assert!(!crate_root.contains("mod raw_account;"));
assert!(!runtime.contains("impl ksp_store_api::RawAccount"));
assert!(!std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/raw_account.rs").exists());

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/migration.rs
// version: 7
// version: 8
fn applied(version: i64, name: &str, checksum: &str) -> super::AppliedMigration {
return super::AppliedMigration { checksum: checksum.to_owned(), name: name.to_owned(), version };
@@ -10,6 +10,7 @@ fn embedded(version: i64, name: &'static str, resources: &'static [crate::Schema
checksum: super::MigrationChecksum::Resources,
hook: super::MigrationHook::None,
name,
previous_checksums: &[],
resources,
version,
};
@@ -36,7 +37,7 @@ fn pre_003_fix_001_embedded_registry_keeps_v000_checksum_and_uses_resource_owned
}
#[test]
fn pre_002_v002_registry_is_additive_minimal_and_keeps_v000_v001_checksums_stable() {
fn pre_003_v002_registry_is_complete_and_keeps_v000_v001_checksums_stable() {
assert_eq!(super::EMBEDDED_MIGRATIONS.len(), 3);
let v000 = &super::EMBEDDED_MIGRATIONS[0];
let v001 = &super::EMBEDDED_MIGRATIONS[1];
@@ -46,7 +47,9 @@ fn pre_002_v002_registry_is_additive_minimal_and_keeps_v000_v001_checksums_stabl
assert_eq!(v002.version, 2);
assert_eq!(v002.name, "raw_account_state");
assert_eq!(v002.hook, super::MigrationHook::None);
assert_eq!(v002.resources.len(), 5);
assert_eq!(v002.previous_checksums, &[super::V002_PROVISIONAL_CHECKSUM_PRE_002]);
assert_eq!(v002.resources.len(), 32);
assert_eq!(super::migration_checksum(v002), "ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e");
assert!(super::validate_embedded_registry(super::EMBEDDED_MIGRATIONS).is_ok());
assert_eq!(crate::current_migration_version(), 2);
let full = [
@@ -54,7 +57,30 @@ fn pre_002_v002_registry_is_additive_minimal_and_keeps_v000_v001_checksums_stabl
applied(1, v001.name, super::migration_checksum(v001).as_str()),
applied(2, v002.name, super::migration_checksum(v002).as_str()),
];
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS).ok(), std::option::Option::Some(3));
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(3));
return;
}
#[test]
fn pre_003_v002_provisional_checksum_requires_schema_autoupdate_and_rejects_unknown_history() {
let v000 = &super::EMBEDDED_MIGRATIONS[0];
let v001 = &super::EMBEDDED_MIGRATIONS[1];
let v002 = &super::EMBEDDED_MIGRATIONS[2];
let provisional = [
applied(0, v000.name, super::migration_checksum(v000).as_str()),
applied(1, v001.name, super::migration_checksum(v001).as_str()),
applied(2, v002.name, super::V002_PROVISIONAL_CHECKSUM_PRE_002),
];
let strict = super::validate_history(&provisional, super::EMBEDDED_MIGRATIONS, false);
assert_eq!(strict.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
assert_eq!(super::validate_history(&provisional, super::EMBEDDED_MIGRATIONS, true).ok(), std::option::Option::Some(3));
let unknown = [
applied(0, v000.name, super::migration_checksum(v000).as_str()),
applied(1, v001.name, super::migration_checksum(v001).as_str()),
applied(2, v002.name, "unknown-v002-checksum"),
];
let unknown_result = super::validate_history(&unknown, super::EMBEDDED_MIGRATIONS, true);
assert_eq!(unknown_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
return;
}
@@ -105,10 +131,10 @@ fn pre_003_fix_001_ordered_registry_accepts_v000_prefix_and_full_v001_history()
let v001 = super::EMBEDDED_MIGRATIONS[1];
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));
assert_eq!(super::validate_history(&prefix, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(1));
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));
assert_eq!(super::validate_history(&full, super::EMBEDDED_MIGRATIONS, false).ok(), std::option::Option::Some(2));
return;
}
@@ -137,7 +163,7 @@ fn pre_003_fix_001_divergent_missing_or_gapped_history_is_terminal_mismatch() {
let missing: [super::AppliedMigration; 0] = [];
let missing_v000 = [applied(1, v001.name, v001_checksum.as_str())];
for history in [&wrong_name[..], &wrong_checksum[..], &missing[..], &missing_v000[..]] {
let result = super::validate_history(history, super::EMBEDDED_MIGRATIONS);
let result = super::validate_history(history, super::EMBEDDED_MIGRATIONS, false);
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::MigrationMismatch));
}
return;
@@ -157,7 +183,7 @@ fn pre_003_fix_001_newer_history_is_rejected_without_down_migration() {
applied(2, v002.name, v002_checksum.as_str()),
applied(3, "future", "future-checksum"),
];
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS);
let result = super::validate_history(&history, super::EMBEDDED_MIGRATIONS, false);
assert_eq!(result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::SchemaNewer));
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/schema.rs
// version: 6
// version: 7
fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn {
return super::ActualColumn {
@@ -49,34 +49,96 @@ fn pre_003_fix_001_v001_resources_are_split_and_idempotent_by_object_family() {
}
#[test]
fn pre_002_v002_resources_are_exactly_two_tables_plus_base_pk_fk_without_indexes_or_domain_checks() {
assert_eq!(crate::V002_RESOURCES.len(), 5);
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[..2], ["tables/001_ksp_raw_account_states.sql", "tables/002_ksp_raw_account_observations.sql"]);
assert_eq!(
ids[2..],
[
"constraints/001_pk_ksp_raw_account_states.sql",
"constraints/002_pk_ksp_raw_account_observations.sql",
"constraints/003_fk_ksp_raw_account_observations_state.sql",
]
);
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 pre.002 physical foundation is missing: {required}");
assert!(normalized_sql.contains(required), "V002 final physical contract is missing: {required}");
}
for forbidden in ["CREATE INDEX", " CHECK ", "ON DELETE CASCADE", "BIGSERIAL", "slot BIGINT", "data TEXT"] {
assert!(!normalized_sql.contains(forbidden), "pre.002 advanced V002 beyond the minimal table/PK/FK scope: {forbidden}");
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;
}