v0.3.3-pre.009-fix.004

This commit is contained in:
2026-08-30 15:05:58 +02:00
parent 5e2f959c01
commit 5f1938c7c3
5 changed files with 182 additions and 8 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 357 # version: 358
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.3-pre.9.fix.3" version = "0.3.3-pre.9.fix.4"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/src/schema.rs // file: crates/ksp-store-postgres-lib/src/schema.rs
// version: 4 // version: 5
/// Immutable V000 physical schema resource inventory. /// Immutable V000 physical schema resource inventory.
pub(crate) const V000_RESOURCES: &[SchemaResource] = &[SchemaResource { 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 { fn normalize_catalog_sql(value: &str) -> std::string::String {
let value = normalize_quoted_numeric_cast_literals(value);
return value return value
.chars() .chars()
.filter(|character| return !character.is_whitespace() && *character != '"' && *character != '(' && *character != ')') .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(); .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<T>(phase: &'static str) -> std::result::Result<T, crate::PostgresBackendError> { fn schema_incompatible<T>(phase: &'static str) -> std::result::Result<T, crate::PostgresBackendError> {
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, phase)); return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::MigrationMismatch, phase));
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-postgres-lib/unit_tests/schema.rs // 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 { fn actual_column(name: &str, udt_name: &str, nullable: bool) -> super::ActualColumn {
return 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() { 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))"); let normalized = super::normalize_catalog_sql("CHECK ((slot >= (0)::numeric) AND (slot <= (18446744073709551615)::numeric))");
assert_eq!(normalized, "checkslot>=0andslot<=18446744073709551615"); 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"); 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"); assert!(resource.is_some(), "V001 slot constraint resource must remain embedded");
let resource = match resource { let resource = match resource {

View File

@@ -0,0 +1,117 @@
<!-- file: deltas/0.3.3/pre.009-fix.004.md -->
<!-- version: 1 -->
# 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
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md --> <!-- file: docs/validation/020-V0_3_3_STORE_POSTGRES_RAW_TRANSACTION.md -->
<!-- version: 17 --> <!-- version: 18 -->
# Validation `0.3.3` — Store/PostgreSQL RawTransaction vertical slice # 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 ; - `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 ; - 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 ; - 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/...` ; - `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 ;
- 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. - 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` ### `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] 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] 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é ; - [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`.