diff --git a/Cargo.toml b/Cargo.toml index 7a74a6e..1746525 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 358 +# version: 359 [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.3-pre.9.fix.4" +version = "0.3.3-pre.9.fix.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-postgres-lib/src/schema.rs b/crates/ksp-store-postgres-lib/src/schema.rs index 370c104..2c6ff85 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: 5 +// version: 7 /// Immutable V000 physical schema resource inventory. pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource { @@ -1138,7 +1138,7 @@ fn matches_expected_constraint_definition(table: &str, kind: &str, definition: & } fn normalize_catalog_sql(value: &str) -> std::string::String { - let value = normalize_quoted_numeric_cast_literals(value); + let value = normalize_quoted_integral_cast_literals(value); return value .chars() .filter(|character| return !character.is_whitespace() && *character != '"' && *character != '(' && *character != ')') @@ -1147,11 +1147,15 @@ fn normalize_catalog_sql(value: &str) -> std::string::String { .replace("::numeric", "") .replace("::bigint", "") .replace("::smallint", "") + .replace("::integer", "") + .replace("::int8", "") + .replace("::int4", "") + .replace("::int2", "") .to_ascii_lowercase(); } -fn normalize_quoted_numeric_cast_literals(value: &str) -> std::string::String { - const NUMERIC_CAST: &str = "::numeric"; +fn normalize_quoted_integral_cast_literals(value: &str) -> std::string::String { + const INTEGRAL_CASTS: &[&str] = &["::bigint", "::int2", "::int4", "::int8", "::integer", "::numeric", "::smallint"]; let mut normalized = std::string::String::with_capacity(value.len()); let mut remaining = value; loop { @@ -1172,15 +1176,24 @@ fn normalize_quoted_numeric_cast_literals(value: &str) -> std::string::String { }, }; let has_digit = literal.chars().any(|character| return character.is_ascii_digit()); - let numeric_literal = !literal.is_empty() + let integral_literal = !literal.is_empty() && has_digit && literal .chars() .enumerate() .all(|(offset, character)| return character.is_ascii_digit() || (offset == 0 && (character == '+' || character == '-'))); - if numeric_literal && let std::option::Option::Some(after_numeric_cast) = after_literal.strip_prefix(NUMERIC_CAST) { + let mut after_integral_cast = std::option::Option::None; + if integral_literal { + for cast in INTEGRAL_CASTS { + if let std::option::Option::Some(value) = after_literal.strip_prefix(*cast) { + after_integral_cast = std::option::Option::Some(value); + break; + } + } + } + if let std::option::Option::Some(value) = after_integral_cast { normalized.push_str(literal); - remaining = after_numeric_cast; + remaining = value; continue; } normalized.push('\''); diff --git a/crates/ksp-store-postgres-lib/unit_tests/schema.rs b/crates/ksp-store-postgres-lib/unit_tests/schema.rs index 689dec9..4a5c6f8 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: 3 +// version: 4 fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn { return super::ActualColumn { @@ -101,6 +101,19 @@ fn pre_003_fix_001_catalog_normalization_and_resource_owned_constraint_definitio assert_eq!(normalized, "checkslot>=0andslot<=18446744073709551615"); let postgres_17_normalized = super::normalize_catalog_sql("CHECK ((slot >= '0'::numeric) AND (slot <= '18446744073709551615'::numeric))"); assert_eq!(postgres_17_normalized, normalized); + let bigint_normalized = super::normalize_catalog_sql( + "CHECK ((block_time_unix_millis IS NULL) OR ((block_time_unix_millis >= '0'::bigint) AND (block_time_unix_millis <= '253402300799999'::bigint)))", + ); + let block_time_resource = crate::V001_RESOURCES.iter().find(|resource| return resource.id == "constraints/007_ck_ksp_raw_transactions_block_time.sql"); + assert!(block_time_resource.is_some(), "V001 block-time constraint resource must remain embedded"); + let block_time_resource = match block_time_resource { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let block_time_expected = super::expected_constraint_definition(block_time_resource.sql, "ck_ksp_raw_transactions_block_time"); + assert_eq!(block_time_expected.as_deref(), std::option::Option::Some(bigint_normalized.as_str())); + let integer_alias = super::normalize_catalog_sql("CHECK (format_version <= '4294967295'::int8)"); + assert_eq!(integer_alias, "checkformat_version<=4294967295"); let text_literal = super::normalize_catalog_sql("CHECK (retention_state = 'full'::text)"); assert_eq!(text_literal, "checkretention_state='full'"); let resource = crate::V001_RESOURCES.iter().find(|resource| return resource.id == "constraints/006_ck_ksp_raw_transactions_slot.sql"); diff --git a/deltas/0.3.3/pre.009-fix.005.md b/deltas/0.3.3/pre.009-fix.005.md new file mode 100644 index 0000000..596b21b --- /dev/null +++ b/deltas/0.3.3/pre.009-fix.005.md @@ -0,0 +1,128 @@ + + + +# Delta `0.3.3-pre.009-fix.005` — canonicalisation des littéraux numériques/intégraux PostgreSQL + +## 1. Base et diagnostic réel + +Base opérateur : + +```text +0.3.3-pre.9.fix.4 +``` + +Le gate standard `pre.009-fix.004` fourni le 2026-08-30 est entièrement propre. Le live PostgreSQL 17 confirme que la correction du CHECK `slot` est effective puis avance jusqu'à la ressource suivante : + +```text +constraints/007_ck_ksp_raw_transactions_block_time.sql +``` + +La ressource crée : + +```sql +CHECK ( + block_time_unix_millis IS NULL + OR block_time_unix_millis >= 0 AND block_time_unix_millis <= 253402300799999 +) +``` + +`block_time_unix_millis` est `BIGINT`. `pg_get_constraintdef` reconstruit une expression depuis la représentation catalogue au lieu de restituer le texte SQL original ; une constante entière typée peut donc apparaître avec un cast explicite. La normalisation de `fix.004` savait retirer les quotes uniquement pour `::numeric` et pouvait encore considérer comme différente une forme telle que : + +```text +'253402300799999'::bigint +``` + +## 2. Version + +```text +workspace.package.version = 0.3.3-pre.9.fix.5 +``` + +## 3. Correction d'introspection + +`schema.rs` généralise la canonicalisation ciblée des littéraux entiers quotés. Une chaîne n'est déquotée que si : + +- elle est constituée d'un signe ASCII optionnel suivi uniquement de chiffres ASCII ; +- elle est immédiatement suivie d'un cast numérique/intégral PostgreSQL retenu ; +- le cast appartient à `numeric`, `bigint`, `integer`, `smallint`, `int8`, `int4` ou `int2`. + +Les casts équivalents non quotés sont également supprimés par la normalisation générale. + +Cette règle reste volontairement distincte des littéraux métier/textuels : + +```text +'full'::text +'archived'::text +'purged'::text +``` + +conservent leurs quotes dans la forme normalisée. + +## 4. Canaris + +Le test d'équivalence catalogue couvre désormais : + +- le CHECK `slot` avec `'18446744073709551615'::numeric` ; +- le CHECK `block_time` avec `'0'::bigint` et `'253402300799999'::bigint` ; +- l'alias intégral `::int8` sur la borne `format_version` ; +- la préservation du littéral texte `'full'::text`. + +Le but est de normaliser des représentations typées équivalentes, jamais de rendre un CHECK divergent compatible. + +## 5. Pourquoi V001 reste inchangée + +Comme pour `slot`, PostgreSQL applique la contrainte avant que KSP ne la rejette lors de la relecture post-apply. Le défaut reste donc dans l'équivalence de catalogue. Modifier le SQL V001 créerait une migration logique différente sans corriger la cause. + +Checksums attendus inchangés : + +```text +V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 +``` + +## 6. Scope + +Aucun changement de : + +- ressource SQL V000/V001 ; +- ordre/nombre des 40 ressources V001 ; +- runtime RawTransaction ; +- pagination/cursor/rétention ; +- Store API/façade ; +- Config ; +- scope `RawAccountState`. + +## 7. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-store-postgres-lib/src/schema.rs +crates/ksp-store-postgres-lib/unit_tests/schema.rs +docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md +deltas/0.3.3/pre.009-fix.005.md +``` + +Aucune suppression. + +## 8. Gate opérateur attendu + +```bash +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.3 +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 +``` + +Puis : + +```bash +read -rsp "Dedicated PostgreSQL URI: " KSP_PG_TEST_URI; echo +printf '%s\n' "$KSP_PG_TEST_URI" | cargo test -p ksp-store-postgres-lib --test postgres_raw_transaction_live -- --ignored --nocapture --test-threads=1 +unset KSP_PG_TEST_URI +``` diff --git a/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md b/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md index 9317bd9..0bebfbd 100644 --- a/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md +++ b/docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.3` — Store/PostgreSQL RawTransaction vertical slice @@ -649,7 +649,11 @@ cap 500/1000 dans Store pagination - gate Cargo standard de `pre.009-fix.003` du 2026-08-30 : PASS complet ; - live PostgreSQL réel `pre.009-fix.003` : PostgreSQL 17 joint puis FAIL précisément sur `constraints/006_ck_ksp_raw_transactions_slot.sql` ; - diagnostic : le CHECK est appliqué, mais la forme catalogue PostgreSQL peut rendre la borne `NUMERIC` hors bigint comme littéral quoté `'18446744073709551615'::numeric`, tandis que l'ancien normaliseur conservait les quotes ; -- `pre.009-fix.004` corrige uniquement cette équivalence catalogue ciblée et ajoute un canari qui préserve les quotes des littéraux texte ; aucune ressource SQL/migration n'est modifiée. +- `pre.009-fix.004` corrige l'équivalence catalogue des littéraux `NUMERIC` quotés et ajoute un canari qui préserve les quotes des littéraux texte ; aucune ressource SQL/migration n'est modifiée ; +- gate Cargo standard `pre.009-fix.004` du 2026-08-30 : PASS complet ; +- live PostgreSQL réel `pre.009-fix.004` : le CHECK `slot` passe désormais et l'échec se déplace précisément sur `constraints/007_ck_ksp_raw_transactions_block_time.sql` ; +- diagnostic : `block_time_unix_millis` est `BIGINT`, et la reconstruction catalogue peut typer une grande constante entière sous forme de littéral quoté suivi d'un cast intégral ; la normalisation ciblée de `fix.004` ne couvrait encore que `::numeric` ; +- `pre.009-fix.005` généralise uniquement cette canonicalisation aux casts numériques/intégraux PostgreSQL (`numeric`, `bigint`, `integer`, `smallint`, `int8`, `int4`, `int2`) sans retirer les quotes des littéraux textuels. ### `pre.010` @@ -779,7 +783,17 @@ cargo check -p ksp-store-lib --no-default-features - [PASS opérateur] gate standard `pre.009-fix.003` intégralement propre le 2026-08-30 ; - [DIAGNOSTIC LIVE] PostgreSQL 17 identifie exactement `constraints/006_ck_ksp_raw_transactions_slot.sql` comme ressource incompatible immédiatement après application ; -- [FIX statique] l'introspection normalise désormais uniquement les littéraux numériques quotés suivis de `::numeric`, forme que le déparseur PostgreSQL peut produire pour `NUMERIC` hors plage bigint ; -- [PASS statique] le canari couvre `'18446744073709551615'::numeric` et vérifie que les littéraux texte comme `'full'::text` conservent leurs quotes ; -- [PASS statique] aucun fichier SQL V000/V001, ordre de ressource ou checksum de migration n'est modifié ; +- [FIX] l'introspection normalise les littéraux numériques quotés suivis de `::numeric`, forme reconstruite pour la borne `NUMERIC` hors plage bigint ; +- [PASS] le canari couvre `'18446744073709551615'::numeric` et vérifie que les littéraux texte comme `'full'::text` conservent leurs quotes ; +- [PASS] aucun fichier SQL V000/V001, ordre de ressource ou checksum de migration n'est modifié ; +- [PASS opérateur] gate Cargo complet `pre.009-fix.004` fourni le 2026-08-30 ; +- [DIAGNOSTIC LIVE] le CHECK `slot` est désormais accepté ; PostgreSQL 17 poursuit V001 puis s'arrête sur `constraints/007_ck_ksp_raw_transactions_block_time.sql`. + +### `pre.009-fix.005` — canonicalisation des littéraux numériques/intégraux typés + +- [DIAGNOSTIC] `block_time_unix_millis` est `BIGINT` et sa borne haute `253402300799999` est une constante entière large ; le catalogue reconstruit les expressions, il ne restitue pas nécessairement le texte SQL source ; +- [FIX statique] le normaliseur retire quotes + cast uniquement pour un littéral ASCII intégral (`[+-]?[0-9]+`) suivi de `::numeric`, `::bigint`, `::integer`, `::smallint`, `::int8`, `::int4` ou `::int2` ; +- [PASS statique] le canari couvre explicitement le CHECK `block_time` sous forme `'253402300799999'::bigint` ainsi qu'un alias `::int8` ; +- [PASS statique] les quotes des littéraux texte restent préservées et aucune comparaison métier n'est relâchée ; +- [PASS statique] aucune ressource SQL V000/V001 ni checksum de migration n'est modifié ; - [À FAIRE] gate Cargo opérateur complet puis live PostgreSQL `postgres_raw_transaction_live`.