diff --git a/Cargo.toml b/Cargo.toml index 408a99c..7a74a6e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 357 +# version: 358 [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.3" +version = "0.3.3-pre.9.fix.4" 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 3ca0616..370c104 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: 4 +// version: 5 /// Immutable V000 physical schema resource inventory. pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource { @@ -1138,6 +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); return value .chars() .filter(|character| return !character.is_whitespace() && *character != '"' && *character != '(' && *character != ')') @@ -1149,6 +1150,47 @@ fn normalize_catalog_sql(value: &str) -> std::string::String { .to_ascii_lowercase(); } +fn normalize_quoted_numeric_cast_literals(value: &str) -> std::string::String { + const NUMERIC_CAST: &str = "::numeric"; + let mut normalized = std::string::String::with_capacity(value.len()); + let mut remaining = value; + loop { + let (before_quote, after_quote) = match remaining.split_once('\'') { + std::option::Option::Some(value) => value, + std::option::Option::None => { + normalized.push_str(remaining); + break; + }, + }; + normalized.push_str(before_quote); + let (literal, after_literal) = match after_quote.split_once('\'') { + std::option::Option::Some(value) => value, + std::option::Option::None => { + normalized.push('\''); + normalized.push_str(after_quote); + break; + }, + }; + let has_digit = literal.chars().any(|character| return character.is_ascii_digit()); + let numeric_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) { + normalized.push_str(literal); + remaining = after_numeric_cast; + continue; + } + normalized.push('\''); + normalized.push_str(literal); + normalized.push('\''); + remaining = after_literal; + } + return normalized; +} + fn schema_incompatible(phase: &'static str) -> std::result::Result { return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, phase)); } diff --git a/crates/ksp-store-postgres-lib/unit_tests/schema.rs b/crates/ksp-store-postgres-lib/unit_tests/schema.rs index 093d4ba..689dec9 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: 2 +// version: 3 fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn { return super::ActualColumn { @@ -99,6 +99,10 @@ fn pre_003_fix_001_required_column_matching_ignores_integer_precision_but_requir fn pre_003_fix_001_catalog_normalization_and_resource_owned_constraint_definition_are_deterministic() { let normalized = super::normalize_catalog_sql("CHECK ((slot >= (0)::numeric) AND (slot <= (18446744073709551615)::numeric))"); assert_eq!(normalized, "checkslot>=0andslot<=18446744073709551615"); + let postgres_17_normalized = super::normalize_catalog_sql("CHECK ((slot >= '0'::numeric) AND (slot <= '18446744073709551615'::numeric))"); + assert_eq!(postgres_17_normalized, normalized); + let text_literal = super::normalize_catalog_sql("CHECK (retention_state = 'full'::text)"); + assert_eq!(text_literal, "checkretention_state='full'"); let resource = crate::V001_RESOURCES.iter().find(|resource| return resource.id == "constraints/006_ck_ksp_raw_transactions_slot.sql"); assert!(resource.is_some(), "V001 slot constraint resource must remain embedded"); let resource = match resource { diff --git a/deltas/0.3.3/pre.009-fix.004.md b/deltas/0.3.3/pre.009-fix.004.md new file mode 100644 index 0000000..54daa91 --- /dev/null +++ b/deltas/0.3.3/pre.009-fix.004.md @@ -0,0 +1,117 @@ + + + +# Delta `0.3.3-pre.009-fix.004` — normalisation du CHECK NUMERIC déparsée par PostgreSQL 17 + +## 1. Base et diagnostic réel + +Base opérateur : + +```text +0.3.3-pre.9.fix.3 +``` + +Le gate standard fourni le 2026-08-30 est entièrement propre. Le live PostgreSQL réel atteint PostgreSQL 17 puis identifie précisément la ressource V001 rejetée après application : + +```text +constraints/006_ck_ksp_raw_transactions_slot.sql +``` + +La ressource crée : + +```sql +CHECK (slot >= 0 AND slot <= 18446744073709551615) +``` + +La colonne `slot` est `NUMERIC(20,0)`. PostgreSQL peut déparser un littéral `NUMERIC` hors plage bigint sous forme quotée avec cast explicite, notamment : + +```text +'18446744073709551615'::numeric +``` + +L'ancien `normalize_catalog_sql` supprimait `::numeric` mais conservait les quotes simples. Le contrat embarqué devenait donc `18446744073709551615` tandis que le catalogue pouvait devenir `'18446744073709551615'`, ce qui produisait un faux `Incompatible` immédiatement après création. + +## 2. Version + +```text +workspace.package.version = 0.3.3-pre.9.fix.4 +``` + +## 3. Correction d'introspection + +`schema.rs` ajoute une normalisation ciblée avant la normalisation générale : + +- seules les chaînes composées d'un signe ASCII optionnel et de chiffres ASCII ; +- uniquement lorsqu'elles sont immédiatement suivies de `::numeric` ; +- voient leurs quotes simples et leur cast `::numeric` retirés dans cette étape ciblée ; +- les autres casts continuent d’être traités par la normalisation existante. + +Cette règle ne retire donc pas les quotes des littéraux métier/textuels tels que : + +```text +'full'::text +'archived'::text +'purged'::text +``` + +Le canari unitaire existant du CHECK `slot` couvre désormais explicitement la forme PostgreSQL 17 quotée et vérifie également qu'un littéral texte conserve ses quotes. + +## 4. Pourquoi la migration reste inchangée + +Le SQL V001 est sémantiquement correct et PostgreSQL l'applique avant que KSP ne le rejette. Le défaut est donc dans l'équivalence du catalogue, pas dans la contrainte elle-même. Modifier la ressource SQL ferait inutilement varier le checksum V001 et réécrirait le contrat de migration pour un problème de déparsing. + +`fix.004` ne modifie donc aucune ressource V000/V001. + +## 5. Invariants + +Aucun changement de : + +- tables, contraintes ou indexes V001 ; +- ordre ou nombre des 40 ressources V001 ; +- Store API / façade ; +- RawTransaction read/write/pagination/rétention ; +- cursor ; +- Config ; +- scope `RawAccountState`. + +Checksums attendus inchangés : + +```text +V000 d29068b8c13b9dc0cc9ef6aaadd0fa12d41e0fe4c56541a1118c4bfc846a1450 +V001 31488cda2f08f3f46c4cdbdbb6c18c243662fada02eac4487040c8735d72cc51 +``` + +## 6. 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.004.md +``` + +Aucune suppression. + +## 7. 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 8164e25..9317bd9 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 @@ -645,8 +645,11 @@ cap 500/1000 dans Store pagination - `pre.009-fix.002` corrige uniquement les cinq diagnostics Clippy du test et propage `PostgresBackendError::phase()` vers `LiveFailure` lors de `open_backend`, sans rendre le texte PostgreSQL, l'URI, SQLSTATE ou bind ; - gate Cargo standard de `pre.009-fix.002` du 2026-08-30 : PASS complet, audits/check/Clippy/tests/no-default-features propres ; - live PostgreSQL réel `pre.009-fix.002` : PostgreSQL 17 joint puis FAIL à `schema_resource_post_apply`, ce qui localise le défaut dans la relecture d'une ressource V001 juste après son application ; -- `pre.009-fix.003` conserve la redaction mais remplace, uniquement pour ce cas post-apply, la phase agrégée par `resource.id`, identifiant embarqué statique et non sensible ; le prochain live doit donc nommer exactement `tables/...`, `constraints/...` ou `indexes/...` ; -- aucune logique SQL/migration n'est modifiée par `fix.003` : il s'agit d'une instrumentation diagnostique sûre avant correction de la ressource réellement fautive. +- `pre.009-fix.003` conserve la redaction mais remplace, uniquement pour ce cas post-apply, la phase agrégée par `resource.id`, identifiant embarqué statique et non sensible ; +- 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.010` @@ -770,5 +773,13 @@ cargo check -p ksp-store-lib --no-default-features - [PASS statique] les implémentations directes `RawTransactionWrite` / `RawTransactionObservationWrite` restent interdites dans le backend à ce stade ; - [PASS statique] le canari `pre.007` continue d'exiger `DELETE_ARCHIVE_PAYLOAD_SQL` et `DELETE FROM ksp_raw_transaction_archive_payloads` ; - [PASS statique] aucune logique runtime, migration ou checksum V000/V001 modifié ; -- [À FAIRE] gate Cargo opérateur complet de `pre.007-fix.001`. +- [PASS] gate Cargo opérateur complet de `pre.007-fix.001` fourni le 2026-08-30. +### `pre.009-fix.004` — normalisation PostgreSQL 17 du CHECK `slot` + +- [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é ; +- [À FAIRE] gate Cargo opérateur complet puis live PostgreSQL `postgres_raw_transaction_live`.