From a704a5722ef4eaea3ee2cee3b3ca40de9ce4854c Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 30 Aug 2026 21:37:02 +0200 Subject: [PATCH] v0.3.4-pre.003 --- Cargo.toml | 4 +- .../004_ck_ksp_raw_account_states_pubkey.sql | 12 + .../005_ck_ksp_raw_account_states_slot.sql | 12 + ...6_ck_ksp_raw_account_states_state_hash.sql | 12 + ...007_ck_ksp_raw_account_states_lamports.sql | 12 + .../008_ck_ksp_raw_account_states_owner.sql | 12 + ...9_ck_ksp_raw_account_states_rent_epoch.sql | 12 + .../010_ck_ksp_raw_account_states_data.sql | 12 + ...11_ck_ksp_raw_account_observations_key.sql | 12 + ...aw_account_observations_account_pubkey.sql | 12 + ..._raw_account_observations_account_slot.sql | 12 + ...ccount_observations_account_state_hash.sql | 12 + ..._ksp_raw_account_observations_provider.sql | 15 + ..._ksp_raw_account_observations_protocol.sql | 15 + ...ck_ksp_raw_account_observations_method.sql | 15 + ...ck_ksp_raw_account_observations_origin.sql | 12 + ...p_raw_account_observations_received_at.sql | 12 + ...w_account_observations_capture_session.sql | 18 ++ ...sp_raw_account_observations_commitment.sql | 18 ++ ..._ksp_raw_account_observations_endpoint.sql | 18 ++ ...ck_ksp_raw_account_observations_filter.sql | 18 ++ ...p_raw_account_observations_observed_at.sql | 15 + ...sp_raw_account_observations_time_order.sql | 12 + ...p_raw_account_observations_source_hash.sql | 15 + ...p_raw_account_observations_source_size.sql | 15 + ...unt_observations_transaction_signature.sql | 15 + ...raw_account_observations_write_version.sql | 15 + ..._account_states_slot_pubkey_state_hash.sql | 2 + crates/ksp-store-postgres-lib/src/lib.rs | 13 +- .../ksp-store-postgres-lib/src/migration.rs | 82 ++++- crates/ksp-store-postgres-lib/src/schema.rs | 304 +++++++++++++++++- .../tests/dependency_boundary.rs | 14 +- .../unit_tests/migration.rs | 42 ++- .../unit_tests/schema.rs | 92 +++++- deltas/0.3.4/pre.003.md | 187 +++++++++++ ...-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md | 22 +- .../021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md | 41 ++- 37 files changed, 1088 insertions(+), 75 deletions(-) create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql create mode 100644 crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql create mode 100644 deltas/0.3.4/pre.003.md diff --git a/Cargo.toml b/Cargo.toml index b695562..e4bab5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 366 +# version: 367 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.4-pre.2.fix.1" +version = "0.3.4-pre.3" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql new file mode 100644 index 0000000..9f675f5 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/004_ck_ksp_raw_account_states_pubkey.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql new file mode 100644 index 0000000..bb7c0a4 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/005_ck_ksp_raw_account_states_slot.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql new file mode 100644 index 0000000..7d367f2 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/006_ck_ksp_raw_account_states_state_hash.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql new file mode 100644 index 0000000..75c4543 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/007_ck_ksp_raw_account_states_lamports.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql new file mode 100644 index 0000000..619a48a --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/008_ck_ksp_raw_account_states_owner.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql new file mode 100644 index 0000000..8e7350d --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/009_ck_ksp_raw_account_states_rent_epoch.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql new file mode 100644 index 0000000..303c320 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/010_ck_ksp_raw_account_states_data.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql new file mode 100644 index 0000000..31da333 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/011_ck_ksp_raw_account_observations_key.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql new file mode 100644 index 0000000..57de7d8 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/012_ck_ksp_raw_account_observations_account_pubkey.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql new file mode 100644 index 0000000..b41c9e4 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/013_ck_ksp_raw_account_observations_account_slot.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql new file mode 100644 index 0000000..9dda237 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/014_ck_ksp_raw_account_observations_account_state_hash.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql new file mode 100644 index 0000000..c2750d4 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/015_ck_ksp_raw_account_observations_provider.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql new file mode 100644 index 0000000..4f3495f --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/016_ck_ksp_raw_account_observations_protocol.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql new file mode 100644 index 0000000..88567e0 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/017_ck_ksp_raw_account_observations_method.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql new file mode 100644 index 0000000..8145eb7 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/018_ck_ksp_raw_account_observations_origin.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql new file mode 100644 index 0000000..8f56551 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/019_ck_ksp_raw_account_observations_received_at.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql new file mode 100644 index 0000000..99a4870 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/020_ck_ksp_raw_account_observations_capture_session.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql new file mode 100644 index 0000000..8f7c57e --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/021_ck_ksp_raw_account_observations_commitment.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql new file mode 100644 index 0000000..409889a --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/022_ck_ksp_raw_account_observations_endpoint.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql new file mode 100644 index 0000000..d5a9106 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/023_ck_ksp_raw_account_observations_filter.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql new file mode 100644 index 0000000..f28ee1a --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/024_ck_ksp_raw_account_observations_observed_at.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql new file mode 100644 index 0000000..e6da30c --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/025_ck_ksp_raw_account_observations_time_order.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql new file mode 100644 index 0000000..dccc5e7 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/026_ck_ksp_raw_account_observations_source_hash.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql new file mode 100644 index 0000000..1f07e2c --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/027_ck_ksp_raw_account_observations_source_size.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql new file mode 100644 index 0000000..15f8360 --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/028_ck_ksp_raw_account_observations_transaction_signature.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql new file mode 100644 index 0000000..29ecf7e --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/constraints/029_ck_ksp_raw_account_observations_write_version.sql @@ -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$; diff --git a/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql new file mode 100644 index 0000000..0e94fcd --- /dev/null +++ b/crates/ksp-store-postgres-lib/migrations/v002_raw_account_state/indexes/001_ix_ksp_raw_account_states_slot_pubkey_state_hash.sql @@ -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); diff --git a/crates/ksp-store-postgres-lib/src/lib.rs b/crates/ksp-store-postgres-lib/src/lib.rs index 768522e..c81e94f 100644 --- a/crates/ksp-store-postgres-lib/src/lib.rs +++ b/crates/ksp-store-postgres-lib/src/lib.rs @@ -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; diff --git a/crates/ksp-store-postgres-lib/src/migration.rs b/crates/ksp-store-postgres-lib/src/migration.rs index d540c33..72bd076 100644 --- a/crates/ksp-store-postgres-lib/src/migration.rs +++ b/crates/ksp-store-postgres-lib/src/migration.rs @@ -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 { +fn validate_history( + history: &[AppliedMigration], + migrations: &[EmbeddedMigration], + allow_previous_checksums: bool, +) -> std::result::Result { 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; diff --git a/crates/ksp-store-postgres-lib/src/schema.rs b/crates/ksp-store-postgres-lib/src/schema.rs index 234448e..fb7b1bf 100644 --- a/crates/ksp-store-postgres-lib/src/schema.rs +++ b/crates/ksp-store-postgres-lib/src/schema.rs @@ -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 { +/// 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 { 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, diff --git a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs index 186fdaf..03fe5ef 100644 --- a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs @@ -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()); diff --git a/crates/ksp-store-postgres-lib/unit_tests/migration.rs b/crates/ksp-store-postgres-lib/unit_tests/migration.rs index 3a49be7..6522eec 100644 --- a/crates/ksp-store-postgres-lib/unit_tests/migration.rs +++ b/crates/ksp-store-postgres-lib/unit_tests/migration.rs @@ -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; } diff --git a/crates/ksp-store-postgres-lib/unit_tests/schema.rs b/crates/ksp-store-postgres-lib/unit_tests/schema.rs index afc60d0..c99713e 100644 --- a/crates/ksp-store-postgres-lib/unit_tests/schema.rs +++ b/crates/ksp-store-postgres-lib/unit_tests/schema.rs @@ -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::>(); - 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::>().concat(); let normalized_sql = sql.split_ascii_whitespace().collect::>().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; } diff --git a/deltas/0.3.4/pre.003.md b/deltas/0.3.4/pre.003.md new file mode 100644 index 0000000..84fec7a --- /dev/null +++ b/deltas/0.3.4/pre.003.md @@ -0,0 +1,187 @@ + + + +# Delta `0.3.4-pre.003` — contraintes, index et compatibilité V002 + +## 1. Base + +Base appliquée : + +```text +0.3.4-pre.002-fix.001 +workspace.package.version = 0.3.4-pre.2.fix.1 +``` + +Le gate opérateur fourni le 2026-08-30 est intégralement vert : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, suites `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, `ksp-config-lib` et `cargo check -p ksp-store-lib --no-default-features` passent. Le backend PostgreSQL exécute notamment 43 tests unitaires sans échec après `pre.002-fix.001`. + +## 2. Version + +Cette tranche contient des changements Rust/SQL fonctionnels : + +```text +workspace.package.version = 0.3.4-pre.3 +label = 0.3.4-pre.003 +``` + +## 3. Scope exécuté + +`pre.003` ferme uniquement le schéma physique V002 et sa compatibilité de migration. Aucun repository account ni dispatch façade n'est ouvert. + +Inventaire V002 final : + +```text +2 tables +29 contraintes = 2 PK + 1 FK + 26 CHECK +1 index non unique +32 resources au total +``` + +Les 26 `CHECK` ajoutés matérialisent les invariants déjà possédés par `ksp-store-api` : + +- pubkeys, state hashes, observation keys et source hashes aux largeurs fixes attendues ; +- transaction signature optionnelle à 64 bytes ; +- `slot`, `lamports`, `rent_epoch`, `account_slot` et `write_version` dans le domaine `u64` exact, avec stockage `NUMERIC(20,0)` lorsque nécessaire ; +- `data` account bornée à 16 MiB, vide autorisé ; +- provenance logique obligatoire/optionnelle bornée à 1..128 octets avec l'alphabet sûr KSP ; +- origin limité à `backfill`, `import`, `live`, `repair`, `replay` ; +- timestamps bornés à `MAX_RAW_UNIX_MILLIS` et ordre `observed_at <= received_at` ; +- source payload size optionnelle bornée à 64 MiB. + +`is_startup` reste un `BOOLEAN NULL` sans contrainte artificielle supplémentaire. + +## 4. Index de navigation + +Un seul index métier est ajouté : + +```text +ix_ksp_raw_account_states_slot_pubkey_state_hash +(slot, pubkey, state_hash) +``` + +Il est non unique et sans prédicat. Aucun index owner/provider/method/time/status n'est introduit sans query publique qui le justifie. + +## 5. Checksum V002 final et transition de prerelease + +Checksum V002 intermédiaire écrit par `pre.002` : + +```text +30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55 +``` + +Checksum V002 final calculé sur les 32 resources ordonnées : + +```text +ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e +``` + +V000 et V001 restent byte-inchangées et conservent leurs checksums : + +```text +V000 = d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +V001 = 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 +``` + +Le moteur accepte le checksum V002 intermédiaire **uniquement** avec `schema_update=update_if_needed`. Dans ce cas, sous la transaction de migration existante : + +1. l'historique reconnaît uniquement le checksum provisoire explicitement connu ; +2. les resources V002 manquantes sont matérialisées et réinspectées ; +3. la compatibilité externe V002 est vérifiée ; +4. l'entrée V002 de `ksp_store_schema_migrations` est remplacée par le checksum final avec un `UPDATE` conditionné par version, nom et ancien checksum ; +5. le commit publie ensemble schéma et historique final. + +Avec `schema_update=disabled`, le checksum provisoire reste un `MigrationMismatch`. Tout checksum V002 inconnu reste rejeté même avec update activé. + +## 6. Schema compatibility V002 + +La vérification externe générique couvre désormais V001 et V002 avec leurs inventaires propres. Pour les deux tables account, elle conserve les mêmes principes que V001 : + +- colonnes attendues et types physiques compatibles ; +- contraintes KSP attendues reconnues par nom/définition ; +- extension externe bloquante rejetée ; +- unique index externe non adossé à une contrainte rejeté ; +- triggers/rules externes rejetés. + +Le probe d'adoption inclut désormais les deux tables V002 afin qu'un schéma KSP account existant sans metadata de migration ne soit pas considéré comme vide. + +Le contrat d'index interne supporte explicitement les deux formes possédées : V001 avec prédicat `retention_state <> 'purged'`, V002 sans prédicat. + +## 7. Canaries et tests modifiés + +Les canaries backend prouvent désormais : + +- 32 resources V002, ordonnées par tables/constraints/index ; +- inventaire exact 2/29/1 ; +- checksum final exact et checksums V000/V001 stables ; +- acceptation conditionnelle du checksum provisoire et rejet des historiques arbitraires ; +- bornes physiques majeures et absence de narrowing `u64` ; +- index unique de navigation non filtré/non unique ; +- compatibilité externe V002 ; +- absence persistante de `src/raw_account.rs`, d'implémentation `RawAccount*` et de dispatch runtime account. + +## 8. Scope négatif préservé + +Cette tranche n'ajoute pas : + +```text +ksp-store-api change +raw_account repository module +RawAccount* implementation on PostgresBackend +RawAccount* implementation on Store +Store account dispatch +account retention/archive/purge +owner/provider/time/status indexes +worker/job/backfill policy +Transport -> Store coupling +new backend +``` + +## 9. Documentation + +`docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md` conserve le forecast souple sous forme de sous-paragraphes `### pre.NNN` / `#### pre.NNN-fix.MMM`. `pre.002-fix.001` y passe au statut gate opérateur complet PASS et `pre.003` au statut réalisé / gate opérateur à rejouer. + +`docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md` enregistre le gate vert de la base, l'inventaire V002 final, les checksums et la stratégie de transition de prerelease. + +## 10. Validation exécutée dans l'environnement d'assemblage + +```text +python3 scripts/audit_rust_workspace_rules.py +=> General Rust rule audit: clean +=> Rust export completeness audit: 0 candidate(s) +=> KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4 +=> Markdown table audit: clean (239 table(s), 138 file(s)) +``` + +Le calcul indépendant de checksum sur les bytes des resources V002 donne : + +```text +ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e +``` + +`cargo`, `clippy` et les tests Rust ne sont pas disponibles dans l'environnement d'assemblage. Aucun résultat Cargo de `pre.003` n'est donc revendiqué ici. + +## 11. Gate opérateur demandé + +```text +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.4 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-api +cargo test -p ksp-store-lib +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-config-lib +cargo check -p ksp-store-lib --no-default-features +``` + +Les preuves PostgreSQL live account restent hors de cette tranche et sont prévues plus tard dans le forecast. + +## 12. Fichiers + +Le delta contient uniquement `Cargo.toml`, les fichiers Rust/tests/schema réellement modifiés, les nouvelles resources V002, les deux documents de suivi modifiés et ce fichier `pre.003.md`. Aucun snapshot complet, aucune archive historique et aucun artefact de build ne sont inclus. + +## 13. Verdict + +`0.3.4-pre.003` : **READY FOR OPERATOR GATE**. diff --git a/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md b/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md index 6c8b162..e13fa5a 100644 --- a/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md +++ b/docs/plans/025-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT_PLAN.md @@ -1,11 +1,11 @@ - + # Plan `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW ## 1. Statut de la release -`0.3.4-pre.001` a figé le design. `0.3.4-pre.002` matérialise uniquement la fondation physique V002 : registry logique, deux tables, deux clés primaires et la FK observation -> state. Aucun repository account, aucun dispatch façade, aucun index métier et aucune contrainte de domaine complète ne sont ouverts dans cette tranche. +`0.3.4-pre.001` a figé le design. `0.3.4-pre.002` a matérialisé la fondation physique V002 minimale puis `pre.002-fix.001` a corrigé deux canaris sans toucher au SQL. `0.3.4-pre.003` complète V002 avec les contraintes de domaine, l'index de navigation, la compatibilité de schéma et le checksum final, toujours sans repository account ni dispatch façade. Base canonique auditée : @@ -17,8 +17,8 @@ workspace.package.version = 0.3.3 Version de travail de cette prerelease : ```text -workspace.package.version = 0.3.4-pre.1 -label = 0.3.4-pre.001 +workspace.package.version = 0.3.4-pre.3 +label = 0.3.4-pre.003 ``` Décision de scope : `ksp-store-api` reste inchangée. L'audit n'a révélé aucun gap backend-agnostic bloquant ; la difficulté restante est exclusivement l'implémentation physique PostgreSQL et son dispatch par la façade. @@ -569,7 +569,7 @@ Remplacement du sizing tabulaire par des sous-sections éditables avec statut ex ### `pre.002` — Registry V002 et tables minimales -**Statut : réalisé ; corrigé par `pre.002-fix.001`, gate opérateur du correctif à rejouer.** +**Statut : réalisé ; corrigé par `pre.002-fix.001`, gate opérateur complet PASS.** Budget cible : **15-20 min**. V002 `raw_account_state` est enregistrée avec exactement cinq resources : deux tables, les PK `(pubkey, slot, state_hash)` / `(observation_key)` et la FK composite observation -> state. Aucun repository account ni dispatch façade n'est ajouté. @@ -577,7 +577,7 @@ Le checksum resources calculable sur cet état intermédiaire est `30ac87496f1bb #### `pre.002-fix.001` — correction des canaris V002 -**Statut : livré ; gate opérateur à rejouer.** +**Statut : réalisé ; gate opérateur complet PASS.** Le gate opérateur de `pre.002` compile le workspace et passe Clippy, Store API, Store façade, Config et `--no-default-features`, mais révèle deux canaris backend obsolètes. Le premier construisait encore un historique « futur » en version 2 alors que V002 occupe désormais cette version ; il est recalibré avec V002 valide suivie d'une version 3 inconnue. Le second comparait la FK V002 à une chaîne SQL sensible aux retours à la ligne ; il normalise désormais uniquement les espaces avant de vérifier le contrat physique. @@ -585,9 +585,15 @@ Aucune ressource SQL, migration, table, contrainte, index, surface runtime ou ca ### `pre.003` — Contraintes, index et schema compatibility V002 -**Statut : planifié.** +**Statut : réalisé ; gate opérateur à rejouer.** -Budget cible : **15-20 min**. Compléter les contraintes physiques, l'index de navigation, la schema compatibility et figer le checksum V002 final, sans avancer les repositories. Cette tranche doit traiter explicitement la transition depuis l'état V002 intermédiaire de `pre.002` avant toute utilisation live persistante. +Budget cible : **15-20 min**. V002 est complétée à **32 resources** : deux tables, 29 contraintes (PK/FK incluses) et un index non unique `(slot, pubkey, state_hash)` sans prédicat. Les contraintes matérialisent les bornes backend-agnostic existantes : fixed-width 32/64 bytes, domaines `u64` via `NUMERIC(20,0)`, data `<= 16 MiB`, codes de provenance 1..128 octets/alphabet sûr, timestamps bornés, ordre `observed_at <= received_at` et source payload `<= 64 MiB`. + +Le checksum V002 final est `ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e`. La transition depuis le checksum provisoire `30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55` est acceptée **uniquement** sous `schema_update=update_if_needed` : le moteur répare/valide d'abord les resources V002 manquantes, vérifie la compatibilité externe puis remplace atomiquement le checksum d'historique dans la même transaction. `schema_update=disabled` conserve un mismatch sûr et tout checksum inconnu reste rejeté. + +Le probe d'adoption reconnaît désormais aussi les deux tables V002. Aucun `src/raw_account.rs`, aucune implémentation `RawAccount*` et aucun dispatch Store ne sont ouverts dans cette tranche. + +Gate de base `pre.002-fix.001` fourni le 2026-08-30 : audits Rust/Markdown, `cargo check --workspace`, Clippy all-targets, suites `ksp-store-api`, `ksp-store-lib`, `ksp-store-postgres-lib`, `ksp-config-lib` et `ksp-store-lib --no-default-features` sont tous PASS. ### `pre.004` — Mapping privé et lectures `get` diff --git a/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md b/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md index bedc9db..fbd2d4d 100644 --- a/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md +++ b/docs/validation/021-V0_3_4_STORE_POSTGRES_RAW_ACCOUNT.md @@ -1,11 +1,11 @@ - + # Validation `0.3.4` — Store/PostgreSQL `RawAccountState` + complétude RAW ## 1. Portée -Cette matrice est ouverte par `0.3.4-pre.001`. `0.3.4-pre.002` matérialise uniquement la fondation V002 : registry, deux tables et PK/FK de base. Son gate opérateur révèle deux canaris unitaires obsolètes sans défaut de migration ; `0.3.4-pre.002-fix.001` les corrige sans modifier les resources SQL. Les contraintes de domaine, l'index, la compatibilité externe complète, les repositories et les quatre capabilities account restent volontairement pending. +Cette matrice est ouverte par `0.3.4-pre.001`. `0.3.4-pre.002` matérialise la fondation V002 minimale et `pre.002-fix.001` corrige deux canaris sans modifier le SQL ; son gate opérateur complet est PASS. `0.3.4-pre.003` complète les contraintes de domaine, l'index de navigation, la compatibilité externe et le checksum V002 final. Les repositories et les quatre capabilities account restent volontairement pending. Base : @@ -272,8 +272,8 @@ Le test live account devra être opt-in/ignored, URI stdin, sans environnement n | Tranche | Objet | État | |---------|-------------------------------------------------------------------|---------| | pre.001 | audit, kbot3, threat model, V002 design, sizing, plan/validation | DONE | -| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | FIX.001 | -| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | PLANNED | +| pre.002 | V002 registry + deux tables + PK/FK de base, sans repository | PASS | +| pre.003 | contraintes complètes, index, schema compatibility, checksum V002 | GATE | | pre.004 | mapping privé state/observation + get reads + hostile rows | PLANNED | | pre.005 | acquisition atomique state+observation + idempotence/conflict | PLANNED | | pre.006 | observation supplémentaire + races/cancellation unitaires | PLANNED | @@ -350,4 +350,35 @@ Invariants du correctif : - aucun module repository account, dispatch Store ou implémentation `RawAccount*` ouvert ; - `workspace.package.version = 0.3.4-pre.2.fix.1` conformément à `VER-ID-007/010`, car des sources Rust de tests sont modifiées. -Gate opérateur du correctif : **À REJOUER**. +Gate opérateur du correctif du 2026-08-30 : **PASS complet** — audits Rust/Markdown, workspace check, Clippy all-targets, Store API, Store façade, backend PostgreSQL, Config et `--no-default-features` sont verts. + +## 19. Verdict `pre.003` + +V002 finale : **PASS statique / gate Cargo opérateur à rejouer**. + +Inventaire final : + +```text +V002 resources = 32 +tables = 2 +constraints = 29 (2 PK + 1 FK + 26 CHECK) +indexes = 1 +final checksum = ff21605ed45f7ab4c0f92bbb692700b4118a9488b04d50a31d259ac59bdb550e +provisional pre.002= 30ac87496f1bb3805d816660891d7eab2127c599a636eb40c17ade5926311f55 +``` + +Contraintes matérialisées : exactitude fixed-width des identités/hash/signature, domaines `u64` en `NUMERIC(20,0)`, data account `<= 16 MiB`, provenance logique 1..128 octets/alphabet sûr, origin fermé aux cinq variantes API, timestamps `0..MAX_RAW_UNIX_MILLIS`, `observed_at <= received_at`, hash source 32 bytes et source size `<= 64 MiB`. `is_startup` reste un booléen nullable sans contrainte artificielle. + +L'unique index métier V002 est non unique et non filtré : + +```text +(slot, pubkey, state_hash) +``` + +Aucun index owner/provider/method/time/status n'est ajouté. + +Compatibilité de prerelease : le checksum provisoire V002 de `pre.002` n'est accepté que lorsque `schema_update=update_if_needed`. Dans ce cas, les resources manquantes sont matérialisées et vérifiées, la compatibilité externe V002 est contrôlée, puis l'entrée d'historique est mise à jour atomiquement vers le checksum final. Avec `schema_update=disabled`, le checksum provisoire est rejeté ; tout checksum non explicitement connu reste `MigrationMismatch`. + +Le probe d'adoption couvre V001 + V002. V000/V001 et leurs checksums restent inchangés. Aucune surface repository/runtime account n'est ouverte : `src/raw_account.rs` absent, aucune implémentation `RawAccount*` et aucun dispatch Store. + +Audits exécutables dans l'environnement d'assemblage : Rust rules **PASS** ; audit Markdown à rejouer après cette mise à jour documentaire. Les commandes Cargo ne sont pas disponibles dans cet environnement et restent donc explicitement à rejouer par l'opérateur.