From 8448ad107919fd05fbfd756ac09245743b7a8aa2 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 11 Aug 2026 22:22:40 +0200 Subject: [PATCH] v0.5.3-pre.002 --- Cargo.toml | 4 +- config/README.md | 4 +- config/exemples/example.store.config.json | 10 +- .../schemas/resolved.app.config.schema.json | 74 +- config/schemas/store.config.schema.json | 74 +- config/store.config.json | 30 +- docs/DEVNET_EXECUTION_GUIDE.md | 8 +- docs/architecture/STORAGE_ARCHITECTURE.md | 38 +- docs/guides/POSTGRES_STORAGE.md | 42 +- .../V0_5_3_KS_STORE_NORMALIZATION_PLAN.md | 128 ++- docs/rules/RULES_RUST.md | 3 +- kb-app-demo-desktop/CHANGELOG.md | 27 +- kb-app-demo-desktop/README.md | 6 +- kb-app-demo-desktop/TODO.md | 3 +- kb-app-demo-desktop/USAGE.md | 22 +- kb-app-demo-desktop/capabilities/default.json | 8 +- .../frontend/demo_core_extraction.html | 6 +- .../frontend/demo_decode_replay.html | 4 +- ..._sql_pg_core.html => demo_store_core.html} | 32 +- ...emo_sql_diag.html => demo_store_diag.html} | 44 +- ...mo_sql_pg_raw.html => demo_store_raw.html} | 30 +- ...html => demo_store_replay_candidates.html} | 22 +- kb-app-demo-desktop/frontend/main.html | 14 +- .../frontend/ts/demo_core_extraction.ts | 10 +- .../frontend/ts/demo_decode_replay.ts | 10 +- .../frontend/ts/demo_execution_solana_core.ts | 30 +- .../frontend/ts/demo_execution_spl.ts | 6 +- ...demo_sql_pg_core.ts => demo_store_core.ts} | 34 +- .../{demo_sql_diag.ts => demo_store_diag.ts} | 46 +- .../{demo_sql_pg_raw.ts => demo_store_raw.ts} | 34 +- ...tes.ts => demo_store_replay_candidates.ts} | 102 +- ...emo_sql_tables.ts => demo_store_tables.ts} | 36 +- kb-app-demo-desktop/frontend/ts/main.ts | 10 +- kb-app-demo-desktop/package.json | 2 +- kb-app-demo-desktop/src/app_state.rs | 23 +- kb-app-demo-desktop/src/demo_backfill.rs | 19 +- kb-app-demo-desktop/src/demo_config.rs | 120 +- .../src/demo_core_extraction.rs | 4 +- kb-app-demo-desktop/src/demo_decode_replay.rs | 32 +- kb-app-demo-desktop/src/demo_devnet_common.rs | 31 +- ...cution_metadata_metaplex_token_metadata.rs | 12 +- .../src/demo_execution_metadata_token_2022.rs | 7 +- .../src/demo_execution_solana_core.rs | 15 +- kb-app-demo-desktop/src/demo_spl_ata.rs | 9 +- kb-app-demo-desktop/src/demo_spl_token.rs | 14 +- .../src/demo_spl_token_2022.rs | 7 +- kb-app-demo-desktop/src/demo_sql_common.rs | 312 ----- kb-app-demo-desktop/src/demo_sql_diag.rs | 73 -- kb-app-demo-desktop/src/demo_sql_pg_core.rs | 44 - kb-app-demo-desktop/src/demo_sql_pg_raw.rs | 44 - kb-app-demo-desktop/src/demo_store_common.rs | 264 +++++ kb-app-demo-desktop/src/demo_store_core.rs | 45 + kb-app-demo-desktop/src/demo_store_diag.rs | 81 ++ kb-app-demo-desktop/src/demo_store_raw.rs | 45 + ...tes.rs => demo_store_replay_candidates.rs} | 170 +-- kb-app-demo-desktop/src/demo_ws.rs | 4 +- kb-app-demo-desktop/src/lib.rs | 114 +- kb-app-demo-desktop/src/splash.rs | 4 +- kb-app-demo-desktop/src/tauri.rs | 110 +- kb-app-demo-desktop/tauri.conf.json | 2 +- kb-app-demo-desktop/vite.config.ts | 10 +- ks-config/CHANGELOG.md | 13 +- ks-config/README.md | 4 +- ks-config/USAGE.md | 4 +- ks-config/src/composition.rs | 142 ++- ks-config/src/environment.rs | 12 +- ks-config/src/execution.rs | 34 +- ks-config/src/lib.rs | 6 +- ks-config/src/listeners.rs | 46 +- ks-config/src/schema.rs | 4 +- ks-config/src/sensitivity.rs | 67 +- ks-config/src/settings.rs | 285 +++-- ks-config/src/store.rs | 36 +- ks-config/src/transport.rs | 60 +- ks-config/src/wallet.rs | 46 +- ks-pipeline-demo-scenarios/CHANGELOG.md | 8 +- ks-pipeline-demo-scenarios/README.md | 4 +- ks-pipeline-demo-scenarios/USAGE.md | 11 +- ks-pipeline-demo-scenarios/src/environment.rs | 166 +-- ks-pipeline-demo-scenarios/src/lib.rs | 30 +- .../collection_verify_campaign.rs | 23 +- .../create_mint_campaign.rs | 23 +- .../devnet_execution.rs | 14 +- .../escrow_campaign.rs | 23 +- .../maintenance_campaign.rs | 23 +- .../pnft_lifecycle_campaign.rs | 23 +- .../print_burn_campaign.rs | 23 +- .../metaplex_token_metadata/use_campaign.rs | 23 +- ks-pipeline-demo-scenarios/src/solana.rs | 22 +- .../src/spl/associated_token_account.rs | 20 +- ks-pipeline-demo-scenarios/src/spl/memo.rs | 22 +- .../src/spl/token/execution.rs | 22 +- .../src/spl/token/lifecycle.rs | 22 +- .../src/spl/token_2022/metadata/campaign.rs | 23 +- ks-store/CHANGELOG.md | 44 +- ks-store/README.md | 55 +- ks-store/TODO.md | 8 +- ks-store/USAGE.md | 163 +-- ks-store/src/constants.rs | 6 +- ks-store/src/contracts.rs | 63 +- ks-store/src/contracts/dto.rs | 35 +- ks-store/src/contracts/dto/event.rs | 143 --- ks-store/src/contracts/dto/ledger.rs | 73 -- ks-store/src/contracts/dto/outcome.rs | 40 + ks-store/src/contracts/dto/store.rs | 207 +++- ks-store/src/contracts/entity.rs | 28 +- ks-store/src/contracts/entity/core.rs | 16 +- ks-store/src/contracts/entity/event.rs | 46 - ks-store/src/contracts/entity/ledger.rs | 37 - ks-store/src/contracts/entity/raw.rs | 8 +- .../replay.rs} | 73 +- ks-store/src/contracts/repository.rs | 45 +- ks-store/src/lib.rs | 364 +++--- ks-store/src/postgres.rs | 185 ++- ks-store/src/postgres/migrations.rs | 362 +++--- ks-store/src/postgres/query.rs | 81 +- .../postgres/query/core_extraction_queries.rs | 18 +- ks-store/src/postgres/query/core_queries.rs | 39 +- .../postgres/query/decode_pipeline_queries.rs | 136 +-- ks-store/src/postgres/query/health_queries.rs | 12 +- ks-store/src/postgres/query/raw_queries.rs | 18 +- .../query/replay_candidate_queries.rs | 79 +- .../query/table_diagnostics_queries.rs | 69 +- .../repository/core_extraction_repository.rs | 15 +- .../repository/core_transaction_repository.rs | 30 +- .../repository/decode_pipeline_repository.rs | 28 +- .../repository/raw_transaction_repository.rs | 20 +- ks-store/src/postgres/store.rs | 503 ++++---- ks-store/src/postgres/test_serial.rs | 4 +- ks-store/src/store.rs | 1023 +++++++++++++++++ scripts/audit_khadhroony_workspace_rules.py | 134 ++- scripts/audit_rust_general_rules.py | 11 +- .../config/example.resolved.app.config.json | 10 +- test-fixtures/config/resolved.app.config.json | 30 +- 134 files changed, 4518 insertions(+), 3595 deletions(-) rename kb-app-demo-desktop/frontend/{demo_sql_pg_core.html => demo_store_core.html} (80%) rename kb-app-demo-desktop/frontend/{demo_sql_diag.html => demo_store_diag.html} (80%) rename kb-app-demo-desktop/frontend/{demo_sql_pg_raw.html => demo_store_raw.html} (81%) rename kb-app-demo-desktop/frontend/{demo_sql_replay_candidates.html => demo_store_replay_candidates.html} (97%) rename kb-app-demo-desktop/frontend/ts/{demo_sql_pg_core.ts => demo_store_core.ts} (51%) rename kb-app-demo-desktop/frontend/ts/{demo_sql_diag.ts => demo_store_diag.ts} (51%) rename kb-app-demo-desktop/frontend/ts/{demo_sql_pg_raw.ts => demo_store_raw.ts} (52%) rename kb-app-demo-desktop/frontend/ts/{demo_sql_replay_candidates.ts => demo_store_replay_candidates.ts} (87%) rename kb-app-demo-desktop/frontend/ts/{demo_sql_tables.ts => demo_store_tables.ts} (58%) delete mode 100644 kb-app-demo-desktop/src/demo_sql_common.rs delete mode 100644 kb-app-demo-desktop/src/demo_sql_diag.rs delete mode 100644 kb-app-demo-desktop/src/demo_sql_pg_core.rs delete mode 100644 kb-app-demo-desktop/src/demo_sql_pg_raw.rs create mode 100644 kb-app-demo-desktop/src/demo_store_common.rs create mode 100644 kb-app-demo-desktop/src/demo_store_core.rs create mode 100644 kb-app-demo-desktop/src/demo_store_diag.rs create mode 100644 kb-app-demo-desktop/src/demo_store_raw.rs rename kb-app-demo-desktop/src/{demo_sql_replay_candidates.rs => demo_store_replay_candidates.rs} (78%) delete mode 100644 ks-store/src/contracts/dto/event.rs delete mode 100644 ks-store/src/contracts/dto/ledger.rs create mode 100644 ks-store/src/contracts/dto/outcome.rs delete mode 100644 ks-store/src/contracts/entity/event.rs delete mode 100644 ks-store/src/contracts/entity/ledger.rs rename ks-store/src/{postgres/replay_candidates.rs => contracts/replay.rs} (87%) create mode 100644 ks-store/src/store.rs diff --git a/Cargo.toml b/Cargo.toml index e4806a0..9c6f872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 68 +# version: 69 [workspace] resolver = "3" @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "0.5.3-pre.1" +version = "0.5.3-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3" diff --git a/config/README.md b/config/README.md index 5f2105b..5c15cf5 100644 --- a/config/README.md +++ b/config/README.md @@ -1,5 +1,5 @@ - + # Configuration locale @@ -111,7 +111,7 @@ Les defaults portent les timeouts, capacités de channels et `auto_reconnect`. U ## Store -`store.config.json` possède la sélection et les paramètres PostgreSQL/SQLite. Le split reste structurel : aucune migration SQL n'est introduite par `0.5.1`. +`store.config.json` possède la sélection du backend et un objet `backend_options` propre au profil sélectionné. `ks-config` conserve ce bloc opaque après composition/résolution ; `ks-store` seul interprète les champs PostgreSQL actuels (`url`, pool, timeout, auto-initialisation). Ajouter un futur moteur ne doit donc pas ajouter ses paramètres au contrat runtime commun tant qu'il n'est pas sélectionné. ## Frontière source/runtime/public/diagnostic diff --git a/config/exemples/example.store.config.json b/config/exemples/example.store.config.json index 10961cb..a7081d3 100644 --- a/config/exemples/example.store.config.json +++ b/config/exemples/example.store.config.json @@ -6,19 +6,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_DEVNET_URL}", "max_connections": 8, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/local.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } } } diff --git a/config/schemas/resolved.app.config.schema.json b/config/schemas/resolved.app.config.schema.json index f56e8b8..11cccda 100644 --- a/config/schemas/resolved.app.config.schema.json +++ b/config/schemas/resolved.app.config.schema.json @@ -59,85 +59,17 @@ "required": [ "enabled", "backend", - "postgres", - "sqlite" + "backend_options" ], "properties": { "enabled": { "type": "boolean" }, "backend": { - "type": "string", - "enum": [ - "postgres", - "sqlite" - ] - }, - "postgres": { - "$ref": "#/$defs/postgres" - }, - "sqlite": { - "$ref": "#/$defs/sqlite" - } - } - }, - "postgres": { - "type": "object", - "additionalProperties": false, - "required": [ - "url", - "max_connections", - "connect_timeout_ms", - "auto_initialize_schema" - ], - "properties": { - "url": { "$ref": "#/$defs/non_empty_string" }, - "max_connections": { - "type": "integer", - "minimum": 1 - }, - "connect_timeout_ms": { - "type": "integer", - "minimum": 1 - }, - "auto_initialize_schema": { - "type": "boolean" - } - } - }, - "sqlite": { - "type": "object", - "additionalProperties": false, - "required": [ - "path", - "create_if_missing", - "busy_timeout_ms", - "max_connections", - "auto_initialize_schema", - "use_wal" - ], - "properties": { - "path": { - "$ref": "#/$defs/non_empty_string" - }, - "create_if_missing": { - "type": "boolean" - }, - "busy_timeout_ms": { - "type": "integer", - "minimum": 1 - }, - "max_connections": { - "type": "integer", - "minimum": 1 - }, - "auto_initialize_schema": { - "type": "boolean" - }, - "use_wal": { - "type": "boolean" + "backend_options": { + "type": "object" } } }, diff --git a/config/schemas/store.config.schema.json b/config/schemas/store.config.schema.json index d439295..9a7b38b 100644 --- a/config/schemas/store.config.schema.json +++ b/config/schemas/store.config.schema.json @@ -31,85 +31,17 @@ "required": [ "enabled", "backend", - "postgres", - "sqlite" + "backend_options" ], "properties": { "enabled": { "type": "boolean" }, "backend": { - "type": "string", - "enum": [ - "postgres", - "sqlite" - ] - }, - "postgres": { - "$ref": "#/$defs/postgres" - }, - "sqlite": { - "$ref": "#/$defs/sqlite" - } - } - }, - "postgres": { - "type": "object", - "additionalProperties": false, - "required": [ - "url", - "max_connections", - "connect_timeout_ms", - "auto_initialize_schema" - ], - "properties": { - "url": { "$ref": "#/$defs/non_empty_string" }, - "max_connections": { - "type": "integer", - "minimum": 1 - }, - "connect_timeout_ms": { - "type": "integer", - "minimum": 1 - }, - "auto_initialize_schema": { - "type": "boolean" - } - } - }, - "sqlite": { - "type": "object", - "additionalProperties": false, - "required": [ - "path", - "create_if_missing", - "busy_timeout_ms", - "max_connections", - "auto_initialize_schema", - "use_wal" - ], - "properties": { - "path": { - "$ref": "#/$defs/non_empty_string" - }, - "create_if_missing": { - "type": "boolean" - }, - "busy_timeout_ms": { - "type": "integer", - "minimum": 1 - }, - "max_connections": { - "type": "integer", - "minimum": 1 - }, - "auto_initialize_schema": { - "type": "boolean" - }, - "use_wal": { - "type": "boolean" + "backend_options": { + "type": "object" } } }, diff --git a/config/store.config.json b/config/store.config.json index 7d76cc8..e14d853 100644 --- a/config/store.config.json +++ b/config/store.config.json @@ -6,19 +6,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_DEVNET_URL}", "max_connections": 8, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/local.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } } }, @@ -27,19 +19,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_MAINNET_URL}", "max_connections": 16, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/mainnet-import.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } } }, @@ -48,19 +32,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_MAINNET_URL}", "max_connections": 16, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/mainnet.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } } } diff --git a/docs/DEVNET_EXECUTION_GUIDE.md b/docs/DEVNET_EXECUTION_GUIDE.md index 06541a0..7eea8e8 100644 --- a/docs/DEVNET_EXECUTION_GUIDE.md +++ b/docs/DEVNET_EXECUTION_GUIDE.md @@ -1,5 +1,5 @@ - + # Guide d’exécution Devnet @@ -30,7 +30,7 @@ Les commandes réutilisables sont centralisées en section 4. Chaque scénario r - le wallet temporaire persistant n’est jamais copié dans les logs ou les preuves ; - le financement est réalisé par un faucet Web Devnet, pas par `solana airdrop`. -Lorsque `database.postgres.auto_initialize_schema` vaut `true`, l’ouverture d’une fenêtre Devnet crée les tables manquantes. Lorsqu’il vaut `false`, le schéma doit être préparé avant l’ouverture. +Depuis `0.5.3-pre.002`, le desktop ne lit plus une section PostgreSQL concrète. Le profil sélectionne `database.backend` et transporte ses paramètres dans `database.backend_options`, interprétés uniquement par `ks-store`. Lorsque l’option backend `auto_initialize_schema` vaut `true`, l’ouverture persistante du `Store` peut initialiser le schéma du backend actif. Lorsqu’elle vaut `false`, le modèle de stockage doit déjà être disponible. ## 3. Organisation des terminaux @@ -81,7 +81,7 @@ Attendre avant tout scénario : - le démarrage de Vite ; - le lancement du binaire `kb-app-demo-desktop` ; - l’ouverture de la fenêtre ; -- le message PostgreSQL indiquant que les 13 tables sont prêtes ; +- le résumé store indiquant que le backend actif est joignable et que les modèles logiques attendus sont prêts ; - la résolution correcte du profil `local_devnet`. Ne pas exécuter les commandes `C06` ou `C07` dans ce terminal. @@ -330,7 +330,7 @@ Un scénario n’est validé que si le replay ne produit ni échec fonctionnel n 1. Dans le terminal B, exécuter `C01`, `C02`, `C03` et `C04`. 2. Dans le terminal B, exécuter `C05` pour repartir d’une base propre. 3. Dans le terminal A, exécuter `T01`. -4. Attendre la création des 13 tables et la disponibilité de la fenêtre. +4. Attendre que le résumé store confirme la disponibilité des modèles logiques attendus, puis la disponibilité de la fenêtre. 5. Dans la fenêtre Configuration, vérifier que `local_devnet` utilise la base Devnet. 6. Dans le terminal B, exécuter le contrôle de tables de `C05`. 7. Financer la pubkey par un faucet Web Devnet si le solde est insuffisant. diff --git a/docs/architecture/STORAGE_ARCHITECTURE.md b/docs/architecture/STORAGE_ARCHITECTURE.md index c62f6bb..6c2de3f 100644 --- a/docs/architecture/STORAGE_ARCHITECTURE.md +++ b/docs/architecture/STORAGE_ARCHITECTURE.md @@ -1,5 +1,5 @@ - + # Architecture du stockage @@ -7,30 +7,38 @@ `ks-store` réunit : +- la façade publique backend-agnostique `Store` et ses options d'ouverture ; - les contrats store-neutral ; - les DTO et entités persistées ; -- la pagination et les rapports de santé ; +- la pagination, le replay et les rapports de santé ; - les traits de repositories ; -- l’implémentation PostgreSQL ; -- les migrations, requêtes et mécanismes de replay associés. +- les résumés runtime/init sanitisés ; +- l’implémentation PostgreSQL privée ; +- les migrations et requêtes appartenant au backend actif. -La consolidation remplace l’ancien découpage entre plusieurs crates de stockage sans supprimer la séparation interne entre contrats et adaptateur PostgreSQL. +Depuis `0.5.3-pre.002`, les consommateurs ne construisent plus un backend concret. `ks-store` possède la connexion et le dispatch interne ; `PostgresStore`, `PgPool` et SQLx ne traversent plus sa frontière publique. ## 2. Frontières ### 2.1 Contrats store-neutral -Les contrats ne doivent pas dépendre des détails SQL lorsqu’une abstraction stable est suffisante. Ils décrivent les entrées, sorties, identifiants, pages et erreurs nécessaires aux consommateurs. +Les contrats ne dépendent pas des détails SQL lorsqu’une abstraction stable est suffisante. Ils décrivent les entrées, sorties, identifiants, pages, diagnostics et erreurs nécessaires aux consommateurs. `StoreOpenOptions` transporte un code backend et un objet d'options opaque ; seul `ks-store` interprète le moteur sélectionné. + +Le résumé runtime expose des modèles logiques et des compteurs d'objets par catégorie sans publier les noms physiques. PostgreSQL peut ainsi exposer des compteurs `table`/`index`, tandis qu'un futur backend peut utiliser d'autres catégories sans modifier le desktop. ### 2.2 Adaptateur PostgreSQL Le module PostgreSQL possède : +- la validation de ses options ; - la connexion et l’initialisation ; - l’application idempotente des migrations ; - les requêtes typées ; -- les diagnostics ; -- les opérations de replay et de sélection de candidats. +- les diagnostics physiques ; +- les opérations de replay et de sélection de candidats ; +- le target de tracing canonique `ks-store`, enrichi des champs `backend="postgres"`, `domain="ks-store.pg"` et `action` pour les opérations PostgreSQL. + +Les détails physiques restent privés. Les erreurs et résumés traversant la façade ne doivent pas divulguer de DSN, password ou option sensible. ### 2.3 Modèles partagés @@ -38,14 +46,14 @@ Les modèles métier communs restent dans `ks-lib` lorsqu’ils dépassent la se ## 3. Catégories de données -Le stockage couvre plusieurs niveaux : +Le stockage distingue quatre niveaux durables : -- données brutes acquises ; -- transactions et instructions canoniques ; -- événements de décodage et diagnostics ; -- matérialisations ; -- états de campagne et candidats de replay ; -- informations opérationnelles et de santé. +- **N1** — acquisition et raw canonique ; +- **N2** — Core Solana canonique ; +- **N3** — decode/materialization générique et journaux versionnés ; +- **N4** — projections spécialisées/queryables additives. + +N1/N2/N3 constituent la fondation fortement gelée après `0.5.3`, sous réserve d'une urgence ou d'une omission structurelle majeure. N4 reste plus évolutif. Les replays N1 -> N2, N2 -> N3 et N3 -> N4 doivent pouvoir être exécutés indépendamment et de manière idempotente. Les noms de tables, contrats de replay et APIs publiques sont documentés dans `ks-store/USAGE.md` à partir des exports et migrations actuels. diff --git a/docs/guides/POSTGRES_STORAGE.md b/docs/guides/POSTGRES_STORAGE.md index 2a7ea81..81f65cf 100644 --- a/docs/guides/POSTGRES_STORAGE.md +++ b/docs/guides/POSTGRES_STORAGE.md @@ -1,32 +1,37 @@ - + # Guide PostgreSQL et contrats de stockage ## Objectif -`ks-store` consolide les contrats de stockage Core, raw, decode et PostgreSQL de bot2 dans une crate unique. +`ks-store` porte la frontière de stockage généraliste. PostgreSQL reste le backend opérationnel actif, mais il est encapsulé derrière la façade publique backend-agnostique `Store`. ## Connexion +Les consommateurs ne construisent plus `PostgresStore`, `PostgresStoreOptions` ni un pool SQLx. Ils transmettent le backend sélectionné et ses options résolues à `StoreOpenOptions`; `ks-store` valide et interprète ensuite les paramètres PostgreSQL. + ```rust -let options = match ks_store::PostgresStoreOptions::new( - database_url, - 10, - 10_000, +let options = match ks_store::StoreOpenOptions::new( true, + "postgres", + serde_json::json!({ + "url": database_url, + "max_connections": 10, + "connect_timeout_ms": 10_000, + "auto_initialize_schema": true + }), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - -let store = match ks_store::PostgresStore::connect(options).await { +let store = match ks_store::Store::open(options).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; ``` -Toujours utiliser `masked_dsn()` dans les diagnostics. +Le descripteur public éventuellement affiché provient du résumé runtime et est déjà masqué par `ks-store`. Le DSN brut et les options backend ne doivent jamais être propagés dans les DTO, erreurs applicatives ou logs normaux. ## Domaines @@ -37,7 +42,9 @@ Toujours utiliser `masked_dsn()` dans les diagnostics. ## Migrations -Les migrations sont idempotentes et ordonnées. Une nouvelle migration ne doit pas modifier rétroactivement une migration déjà publiée. +`0.5.3-pre.002` conserve temporairement le schéma PostgreSQL historique `kb_sol_*` et son mécanisme d'initialisation afin de réaliser d'abord la rupture d'API backend. `pre.003` reconstruit le baseline sous `k_sol_*`, range les ressources sous `ks-store/migrations/postgres/` et impose une instruction SQL par fichier exécutée par les fonctions privées du backend. + +Les bases Devnet/Mainnet/test `0.5.2` sont reconstructibles : aucune compatibilité in-place n'est requise pour ce changement de baseline. ## Compatibilité des identités de processeurs @@ -47,15 +54,20 @@ Aucune migration SQL automatique de ces valeurs n'est ajoutée en `0.5.1` : avan ## Repositories -Les traits publics séparent le contrat de l’implémentation PostgreSQL. Les opérations de lecture utilisent des filtres et paginations bornés. +Les traits publics et la façade `Store` décrivent des opérations fonctionnelles indépendantes du moteur. PostgreSQL implémente ces contrats à l’intérieur de `ks-store`; aucune crate consommatrice ne reçoit `PgPool`, un type SQLx ou une requête concrète. Les opérations de lecture utilisent des filtres et paginations bornés. ## Diagnostics +La surface publique expose : + - health snapshot ; - migration snapshot ; -- backend diagnostics ; -- diagnostics des tables raw, Core et decode ; -- validation des noms de tables. +- backend descriptor masqué ; +- résumé de configuration sanitisé ; +- résumé d'initialisation/vérification par modèle logique ; +- diagnostics de ressources raw/Core/decode/materialization sans nom physique d'objet. + +Les noms de tables, index, contraintes et le SQL restent des détails backend-internes. Les traces PostgreSQL utilisent l’unique `target: crate::TRACING_TARGET` (`ks-store`) avec les champs structurés `backend="postgres"`, `domain="ks-store.pg"` et un `action` précis (`connection`, `migration`, `query`, `health`, `replay`, etc.). ## Invariants @@ -63,7 +75,7 @@ Les traits publics séparent le contrat de l’implémentation PostgreSQL. Les o - les écritures rejouables doivent être idempotentes ; - la progression de campagne doit rester cohérente avec les lignes effectivement traitées ; - les requêtes dynamiques n’acceptent que des identifiants validés ; -- les erreurs PostgreSQL restent distinctes des erreurs de contrat. +- les détails PostgreSQL utiles au diagnostic restent backend-internes ; les erreurs traversant la façade publique ne doivent jamais divulguer DSN, password ou options sensibles. ## Références diff --git a/docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md b/docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md index 1bbe119..edf0953 100644 --- a/docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md +++ b/docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.5.3` — audit et normalisation de `ks-store` @@ -55,7 +55,7 @@ Les décisions suivantes sont retenues pour les tranches suivantes : 14. **`ks-store` ne dépend pas de `ks-config`** : la frontière applicative transporte une configuration résolue, mais l'interprétation du backend et la création de la connexion sont internes à `ks-store` ; 15. **le bloc de configuration backend devient sélectionné et opaque jusqu'à `ks-store`**, afin que `ks-config` n'impose plus simultanément les paramètres de backends non sélectionnés ; 16. **`ks-store` expose un résumé runtime sans secrets** destiné notamment à `kb-app-demo-desktop` : backend actif, état de connexion, options publiques retenues, auto-initialisation, version/état du schéma et tables créées/vérifiées au démarrage ; -17. **les surfaces `demo_sql` du desktop seront adaptées** à cette façade et à ces diagnostics génériques lorsque l'API change ; aucune logique SQL/PostgreSQL ne doit revenir dans l'application ; +17. **les surfaces historiques `demo_sql_*` du desktop sont renommées structurellement en `demo_store_*` dès `pre.002`** ; la refonte fonctionnelle finale des tableaux/compteurs reste planifiée plus tard et aucune logique SQL/PostgreSQL ne doit revenir dans l'application ; 18. **toutes les opérations PostgreSQL portent un target de logging sous `ks-store.pg`** ; le SQL brut peut être journalisé au niveau `trace`, sans valeurs bindées ni secrets, tandis que les niveaux normaux journalisent l'identité d'opération, la durée et le résultat ; 19. **le contrat de page publique abandonne les gros offsets comme mécanisme durable** au profit d'une pagination stable/cursorisée pour les lectures qui en ont besoin ; aucune requête UI ne doit pouvoir demander 100 000 lignes en un appel ; 20. **les index non uniques sont des objets physiques évolutifs**, exclus du gel logique des tables ; les PK, FK, uniques, checks, types, nullabilités et sémantiques de colonnes sont gelés ; @@ -67,7 +67,7 @@ Les décisions suivantes sont retenues pour les tranches suivantes : 26. **les replays inter-niveaux sont des contrats de premier rang** : N1 -> N2, N2 -> N3 et N3 -> N4 doivent pouvoir être rejoués indépendamment, de façon idempotente, versionnée et avec invalidation descendante contrôlée ; 27. **`k_sol_mat_outputs` est conservée impérativement comme journal N3 générique** même lorsque des projections N4 existent ; une projection spécialisée n'est jamais un remplacement de la sortie matérialisée canonique/versionnée ; 28. **les metadata auront à terme deux projections N4 conceptuelles distinctes** : une projection canonique d'asset/token alimentable par Metaplex Token Metadata et Token-2022, et une projection de metadata de programme pour Solana Program Metadata (SPM) ; elles ne sont pas créées en `0.5.3`, mais leur séparation conceptuelle est retenue ; -29. **le desktop ne doit plus exposer le backend concret** : les surfaces `demo_sql` seront refondues vers des résultats tabulaires génériques (avec préférence pour une nomenclature `demo_store_*`), `demo_config` consommera un résumé store sanitisé sans champs PostgreSQL/SQLite concrets, et le splash vérifiera des modèles logiques avec compteurs d'objets sans afficher les noms physiques des tables/index ; +29. **le desktop ne doit plus exposer le backend concret** : les surfaces portent la nomenclature `demo_store_*` dès `pre.002` et seront refondues fonctionnellement vers des résultats tabulaires génériques, `demo_config` consommera un résumé store sanitisé sans champs PostgreSQL/SQLite concrets, et le splash vérifiera des modèles logiques avec compteurs d'objets sans afficher les noms physiques des tables/index ; 30. **la façade `Store` ouverte au démarrage est destinée à être conservée dans l'état applicatif** plutôt que recréée par chaque commande desktop ; `ks-store` possède ainsi la connexion, l'initialisation et les diagnostics runtime, tandis que l'application ne conserve qu'une capacité backend-agnostique ; 31. **le gel N1-N3 est conditionné par un canari de compatibilité avec les futurs mécanismes de décodage**, Anchor/IDL étant le premier cas concret : l'ajout d'un décodeur futur doit pouvoir se faire sans modification structurelle normale de N1/N2/N3. `0.5.3` n'implémente pas Anchor, mais doit vérifier que les niveaux gelés conservent les bytes d'instruction, comptes ordonnés et leurs attributs, hiérarchie CPI, logs, return data lorsqu'elle existe, temporalité, provenance et observations/états de comptes nécessaires à un redécodage ultérieur ; toute omission générique découverte par cet audit doit être corrigée avant gel ; 32. **les 13 tables existantes constituent la baseline auditée, pas un quota de tables finales** : leur séparation top-level/CPI est conservée, mais le nombre final peut augmenter en `0.5.3` si le canari de readiness démontre qu'un fait Solana générique indispensable manque dans N1/N2/N3. Aucune nouvelle table ne doit être ajoutée pour Anchor en tant que technologie ; seules des structures canoniques Solana indépendantes du mécanisme de décodage sont admissibles. @@ -824,31 +824,17 @@ Le contrat visé couvre au minimum : - compteurs d'objets attendus, présents, créés et vérifiés, notamment tables et index lorsque le backend possède ces notions ; - éventuelles actions de migration/init exécutées, identifiées sans SQL sensible ni paramètres secrets. -Le résumé public **n'expose pas les noms physiques des tables, index ou contraintes**. Ces détails restent backend-internes et peuvent être tracés dans `ks-store.pg.migration`. Un backend non relationnel peut produire le même résumé logique avec ses propres objets physiques. +Le résumé public **n'expose pas les noms physiques des tables, index ou contraintes**. Ces détails restent backend-internes et peuvent être tracés sous le target canonique `ks-store` avec les champs structurés `backend="postgres"`, `domain="ks-store.pg"` et `action="migration"`. Un backend non relationnel peut produire le même résumé logique avec ses propres objets physiques. -Les noms exacts (`StoreRuntimeSummary`, `StoreInitializationSummary`, `StoreModelVerificationSummary`, etc.) seront figés lors de `pre.002`. Le desktop consomme uniquement ces DTO génériques. Les panneaux `demo_sql` seront refondus vers des résultats tabulaires génériques et pourront être renommés `demo_store_*`; `demo_config` ne doit plus lire directement la configuration PostgreSQL/SQLite. Le même résumé logique alimente les informations store du splash. +Les noms exacts (`StoreRuntimeSummary`, `StoreInitializationSummary`, `StoreModelVerificationSummary`, etc.) seront figés lors de `pre.002`. Le desktop consomme uniquement ces DTO génériques. Les panneaux portent la nomenclature `demo_store_*` dès `pre.002`; leur présentation tabulaire finale sera refondue en `pre.005`. `demo_config` ne doit plus lire directement la configuration PostgreSQL/SQLite. Le même résumé logique alimente les informations store du splash. ### 18.5 Logging PostgreSQL -Le domaine racine retenu est : - -```text -ks-store.pg -``` - -avec des sous-targets possibles et cohérents : - -```text -ks-store.pg.connection -ks-store.pg.migration -ks-store.pg.query -ks-store.pg.health -ks-store.pg.replay -``` +Le target Rust reste l’unique `crate::TRACING_TARGET` de la crate, soit `ks-store`, conformément aux règles du workspace. Le backend PostgreSQL est distingué par des champs structurés stables : `backend = "postgres"`, `domain = "ks-store.pg"` et `action = ...`. Règles : -- toute exécution SQL PostgreSQL est traçable par un target `ks-store.pg.*` ; +- toute exécution SQL PostgreSQL est traçable sous `target: crate::TRACING_TARGET` avec `backend = "postgres"` et `domain = "ks-store.pg"` ; - `info`/`debug` journalisent l'identité d'opération, le temps d'exécution, le nombre de lignes et l'issue ; - le texte SQL brut peut être journalisé à `trace` pour le diagnostic ; - les valeurs bindées ne sont pas journalisées par défaut ; @@ -1235,7 +1221,7 @@ Le découpage suivant est une **trajectoire fonctionnelle**. Il ne force pas la - supprimer les quatre traits/DTO historiques non implémentés ; - remodeler le bloc store config en backend + options sélectionnées/opaques ; - déplacer toute validation/interprétation PostgreSQL dans `ks-store` ; -- introduire les targets `ks-store.pg.*` et les canaris backend/secret ; +- introduire l’instrumentation PostgreSQL sous le `TRACING_TARGET` canonique avec `backend="postgres"`, `domain="ks-store.pg"` et les canaris backend/secret ; - migrer les helpers de connexion sans changer encore les tables ; - préparer une façade `Store` persistante dans l’état applicatif afin que les commandes desktop ne recréent pas de connexion backend. @@ -1263,12 +1249,12 @@ Le découpage suivant est une **trajectoire fonctionnelle**. Il ne force pas la - ajouter les index justifiés ; - couvrir les bornes BIGINT ; - vérifier atomicité et reprise après erreur ; -- instrumenter les requêtes PostgreSQL sous `ks-store.pg.query`. +- instrumenter les requêtes PostgreSQL sous `target: crate::TRACING_TARGET`, `backend="postgres"`, `domain="ks-store.pg"` et `action` explicite. -### `0.5.3-pre.005` — consommateurs, `demo_sql` et validation PostgreSQL réelle +### `0.5.3-pre.005` — finalisation `demo_store`, consommateurs et validation PostgreSQL réelle - supprimer les derniers `Postgres*` de `ks-pipeline-demo-scenarios` ; -- refaire les surfaces `demo_sql` concernées de `kb-app-demo-desktop` autour de résultats tabulaires génériques indépendants du backend, avec migration de nomenclature vers `demo_store_*` si elle reste cohérente avec la surface UI ; +- finaliser les surfaces `demo_store_*` de `kb-app-demo-desktop` autour de résultats tabulaires génériques indépendants du backend ; la nomenclature technique a déjà été migrée en `pre.002` ; - refaire `demo_config` afin qu'elle n'expose/interprète plus les champs PostgreSQL/SQLite et consomme uniquement le résumé store sanitisé ; - refaire les informations store du splash autour d'une vérification par modèle logique et de compteurs (tables/index attendus, présents, créés), sans afficher les noms physiques ; - confirmer que `ks-pipeline` reste inchangé dans son rôle d'orchestration ; @@ -1346,7 +1332,7 @@ La version ne peut être clôturée que si : - les index ajoutés correspondent à des requêtes réelles ; - aucune API publique de `ks-store` n'expose PostgreSQL ou SQLx ; - aucune crate externe n'interprète les paramètres PostgreSQL ; -- PostgreSQL reste entièrement fonctionnel comme backend actif, avec ses SQL ressources sous `migrations/postgres/` et ses logs sous `ks-store.pg.*` ; +- PostgreSQL reste entièrement fonctionnel comme backend actif, avec ses SQL ressources sous `migrations/postgres/` et ses logs sous le target canonique `ks-store` avec domaine structuré `ks-store.pg` ; - aucun secret n'apparaît dans diagnostics/erreurs/résumés runtime/logs ; - les tables futures sont guidées par des faits canoniques, pas par des Program IDs ; - aucune table DEX spéculative n'a été gelée sans matérialiseur réel ; @@ -1368,18 +1354,102 @@ Les retours de revue de `pre.001` fixent désormais les points suivants : 8. SQL PostgreSQL sous `ks-store/migrations/postgres/`, une instruction par fichier, fonctions privées `include_str!`, tables/keys/index/drop/truncate séparés et orchestrateur d'init ; 9. façade publique `Store` backend-agnostique et PostgreSQL entièrement privé ; 10. configuration backend opaque jusqu'à `ks-store`, sans dépendance `ks-store -> ks-config` ; -11. résumé runtime/init sans secrets exposé à `kb-app-demo-desktop` et réconciliation future de `demo_sql` ; -12. logging PostgreSQL sous `ks-store.pg.*`, sans secrets ni valeurs bindées par défaut ; +11. résumé runtime/init sans secrets exposé à `kb-app-demo-desktop` et renommage structurel immédiat des surfaces `demo_sql_*` vers `demo_store_*` ; +12. logging PostgreSQL sous le target canonique `ks-store` avec `backend="postgres"` et `domain="ks-store.pg"`, sans secrets ni valeurs bindées par défaut ; 13. pagination cursorisée/bornée et distinction gel logique/index physiques ; 14. architecture future préservée et précisée : N1 Worker 1/raw, N2 Worker 2/Core, N3 decode/materialization générique avec `k_sol_mat_outputs`, N4 projections spécialisées/queryables ; 15. les contrats N1/N2/N3 sont fortement gelés après `0.5.3` sauf urgence ou omission structurelle majeure ; les tables/modèles N4 restent plus souples et évolutifs ; 16. replays explicites et indépendants N1 -> N2, N2 -> N3 et N3 -> N4, avec idempotence, version de processor/projector et invalidation descendante ; 17. aucune troisième table `outer` : les instructions historiquement dites outer sont les top-level ; les nouveaux contrats utilisent `top-level` et la table CPI conserve `stack_height` ainsi que le parent CPI immédiat reconstructible ; 18. metadata N4 prévue en deux projections : asset/token metadata commune Metaplex + Token-2022 et program metadata distincte pour SPM, toutes deux reconstructibles depuis N3 ; -19. `demo_sql` évolue vers des résultats tabulaires backend-agnostiques, `demo_config` cesse d'exposer PostgreSQL/SQLite, et le splash vérifie des modèles logiques avec compteurs sans noms de tables/index ; +19. `demo_store_*` devient la nomenclature technique dès `pre.002`; sa présentation finale évolue vers des résultats tabulaires backend-agnostiques, `demo_config` cesse d'exposer PostgreSQL/SQLite, et le splash vérifie des modèles logiques avec compteurs sans noms de tables/index ; 20. la façade `Store` est destinée à être ouverte une fois et conservée dans `AppState`, les commandes desktop réutilisant cette capacité générique ; 21. `pre.007` est un premier candidat de clôture, jamais une obligation : la clôture est décalée si des manques apparaissent ; 22. Anchor reste hors périmètre fonctionnel de `0.5.3`, mais devient un canari explicite du gel : N1/N2/N3 doivent conserver sans perte les informations génériques nécessaires aux futurs decoders (instructions/CPI, comptes ordonnés et flags, logs, `returnData` réelle, observations/états de comptes, temporalité/provenance) et N3 doit pouvoir tracer le decoder + schéma/IDL/version/hash utilisés ; 23. les 13 tables sont la baseline existante, pas un quota final : si ce canari révèle une omission Solana générique, `0.5.3` peut ajouter avant gel un contrat N1/N2/N3 canonique non nommé d'après Anchor. Ces décisions remplacent les propositions initiales de fusion des instructions et de conservation immuable des migrations `0001` à `0004`. `pre.002` peut démarrer sur cette base ; aucune migration SQL massive n'est encore incluse dans `pre.001`. + + +## 32. État réalisé de `0.5.3-pre.002` + +La tranche `0.5.3-pre.002` réalise la première rupture effective de dépendance au backend concret, sans modifier le schéma SQL historique qui reste volontairement réservé à `pre.003`. + +### 32.1 Façade et possession de la connexion + +- `ks-store` expose désormais `Store` et `StoreOpenOptions` comme frontière publique d’ouverture et d’utilisation du stockage ; +- `PostgresStore`, `PostgresStoreOptions`, le pool SQLx et les helpers PostgreSQL restent internes à `ks-store` ; +- `Store` implémente les contrats fonctionnels déjà consommés par les pipelines et délègue au backend actif sans faire remonter le dispatch dans les crates consommatrices ; +- le desktop conserve un `Store` ouvert une seule fois dans `AppState` via `tokio::sync::OnceCell`, puis les commandes réutilisent cette capacité ; +- `ks-pipeline-demo-scenarios` utilise également la façade générique et ne construit plus le backend PostgreSQL. + +### 32.2 Configuration backend opaque + +Le contrat runtime de `ks-config` conserve désormais uniquement : + +```text +enabled +backend +backend_options +``` + +`backend_options` reste un objet JSON opaque pour `ks-config`. La sélection, la validation des paramètres PostgreSQL, la création de la connexion et l’auto-initialisation sont interprétées par `ks-store`. `ks-store` ne dépend pas de `ks-config`. + +PostgreSQL est le seul backend opérationnel de cette tranche. Un backend inconnu ou incomplet est refusé par une erreur structurée qui ne recopie pas ses options sensibles. + +### 32.3 Diagnostics et résumé runtime génériques + +La surface publique fournit des DTO backend-agnostiques pour : + +- configuration sanitisée ; +- backend descriptor masqué ; +- health ; +- état de migration ; +- ressources logiques et statistiques ; +- vérification par modèle logique ; +- résumé d’initialisation ; +- résumé runtime complet. + +Le résumé d’initialisation regroupe actuellement les ressources du schéma historique sous les modèles logiques `raw`, `core`, `processing`, `decode` et `materialization`. Il expose des compteurs de ressources attendues/disponibles/créées sans exposer les noms physiques. Une liste générique de catégories d’objets backend expose en plus les compteurs physiques sans noms : PostgreSQL fournit déjà la catégorie `table`; `pre.003` ajoutera la catégorie `index` lors de la reconstruction/introspection du nouveau baseline. Un futur backend pourra fournir d’autres catégories sans modifier le contrat du desktop. + +La constante `STORE_SCHEMA_CONTRACT_VERSION` identifie explicitement cette étape comme un schéma legacy transitoire ; elle ne constitue pas le snapshot frozen final. + +### 32.4 Replay et API publique + +Les DTO publics de diagnostic et de replay ne portent plus le préfixe `Postgres`. Le vocabulaire nouveau utilise `top-level` au lieu de `outer`. Les quatre anciens traits publics sans implémentation PostgreSQL réelle et leurs DTO historiques sont supprimés au lieu d’être figés. + +La structure SQL et les requêtes restent encore basées sur les tables `kb_sol_*` de `0.5.2` : leur reconstruction sous `k_sol_*`, l’alignement top-level/CPI et le nouveau baseline appartiennent toujours à `pre.003`. + +### 32.5 Desktop + +Les commandes et payloads nécessaires au desktop ne construisent plus un backend concret. `demo_config` reçoit un résumé store sanitisé ; les diagnostics SQL utilisent désormais des ressources logiques génériques et un descriptor de connexion masqué. Le splash utilise le résumé d’initialisation par modèle et des compteurs, sans afficher de noms physiques de tables/index. + +Les noms historiques de fenêtres/commandes `demo_sql_*` sont supprimés dès `pre.002` et remplacés par `demo_store_*`. `pre.005` conserve uniquement la refonte fonctionnelle finale de l’expérience tabulaire, de `demo_config` et du splash autour du nouveau baseline. + +### 32.6 Logging et sécurité + +Le backend PostgreSQL utilise l’unique target canonique de la crate : + +```text +target: crate::TRACING_TARGET // "ks-store" +backend = "postgres" +domain = "ks-store.pg" +action = "connection | migration | query | health | replay | ..." +``` + +Un canari workspace interdit désormais les types/symboles PostgreSQL/SQLx dans les crates consommatrices et vérifie que l’implémentation PostgreSQL utilise `target: crate::TRACING_TARGET` avec les champs structurés `backend="postgres"` et `domain="ks-store.pg"`. Des tests sentinelles vérifient également qu’un backend inconnu, un descriptor masqué et les résumés publics ne recopient pas des options contenant un secret. + +Le nettoyage fin de l’instrumentation des requêtes (durées, nombres de lignes et suppression systématique de toute valeur bindée des traces normales) reste couplé à la réécriture des repositories de `pre.004`, conformément au plan. + +### 32.7 Limites volontaires de la tranche + +`pre.002` ne : + +- renomme aucune table `kb_sol_*` ; +- ne crée aucun contrat N1/N2/N3 supplémentaire ; +- ne restructure pas encore `migrations/postgres/` ; +- ne change pas `block_time`, `stack_height`, la hiérarchie CPI ou `k_sol_mat_outputs` ; +- ne crée aucune projection N4 metadata/trading ; +- n’implémente pas Anchor. + +Ces travaux restent affectés aux tranches `pre.003+` prévues par le présent plan. diff --git a/docs/rules/RULES_RUST.md b/docs/rules/RULES_RUST.md index a36eadb..008b7a5 100644 --- a/docs/rules/RULES_RUST.md +++ b/docs/rules/RULES_RUST.md @@ -1,5 +1,5 @@ - + # Règles Rust générales @@ -101,6 +101,7 @@ implicit_saturating_sub = "allow" ## Visibilité et API de crate - Aucun `pub mod` n'est autorisé. Les modules restent privés et l'API est constituée exclusivement par des réexports explicites depuis le point d’entrée de la crate. +- Aucun `pub(in ...)` ni `pub(super)` n'est autorisé : un élément partagé au-delà de son module est `pub(crate)`, réexporté au crate-root, puis consommé via `crate::Item`; sinon il reste strictement privé. - Un élément `pub` inaccessible depuis le point d'entrée de sa crate est une erreur de conception, pas un simple avertissement. - Un élément `pub(crate)` utilisé hors de son module est réexporté au niveau du point d'entrée de la crate. - Les chemins internes de modules ne font pas partie de l'API stable. diff --git a/kb-app-demo-desktop/CHANGELOG.md b/kb-app-demo-desktop/CHANGELOG.md index 574d909..95da62a 100644 --- a/kb-app-demo-desktop/CHANGELOG.md +++ b/kb-app-demo-desktop/CHANGELOG.md @@ -1,8 +1,33 @@ - + # CHANGELOG — kb-app-demo-desktop +## `0.5.3-pre.002-delta-fix-004` + +- aligne les versions Tauri/Vite de l'application sur `0.5.3` dans `package.json` et `tauri.conf.json` ; +- remplace le groupe visible `SQL` par `Store` dans le menu principal et renomme les entrées backend-spécifiques en `Raw store` / `Core store` ; +- retire les titres de fenêtres `PostgreSQL ...` des surfaces `demo_store_*` et renomme le titre de la page de replay pour rester backend-agnostique ; +- réconcilie les rustdocs, les bindings TS-RS et la documentation d'usage des surfaces desktop afin qu'ils décrivent la façade `Store` et non PostgreSQL/SQL ; +- aligne les messages de préparation Devnet et le texte d'extraction Core sur les ressources/store génériques au lieu de tables ou transactions PostgreSQL. + +## `0.5.3-pre.002-delta-fix-003` + +- corrige `demo_core_extraction_execute`, qui reçoit déjà `&AppState` et ne doit donc pas appeler `state.inner()` avant de réutiliser le `Store` partagé ; +- rend `store_open_options_from_profile` strictement privé à `demo_store_common` et supprime son réexport crate-root devenu inutilisé. + +## `0.5.3-pre.002-delta-fix-002` + +- renomme les modules, commandes Tauri, fenêtres, pages HTML, TypeScript et DTO de `demo_sql_*` vers `demo_store_*` afin que la nomenclature ne dépende plus d’un backend SQL ; +- conserve la refonte fonctionnelle finale des tableaux/compteurs pour `pre.005`. + +## `0.5.3-pre.002` + +- conserve un `ks_store::Store` backend-agnostique dans `AppState` via `OnceCell` et réutilise ce handle dans les commandes de backfill, extraction Core, replay et scénarios d'exécution ; +- remplace les payloads SQL/configuration dépendants de PostgreSQL par des descripteurs de connexion sanitisés et des diagnostics de ressources/modèles génériques ; +- adapte le splash à la vérification du store par modèle logique et compteurs de ressources, sans afficher les noms physiques des tables/index ; +- remplace le vocabulaire replay `outer` par `top-level` dans les nouveaux contrats UI, tout en conservant temporairement les noms techniques des fenêtres `demo_store_*` jusqu'à leur refonte finale. + ## `0.5.2-pre.006-delta-fix-009` - corrige les adaptateurs Tauri Devnet qui transmettaient directement `tauri::State` aux helpers wallet/projection attendant `&AppState`; les commandes empruntent désormais explicitement `&state` pour System, SPL et les trois familles Metadata ; diff --git a/kb-app-demo-desktop/README.md b/kb-app-demo-desktop/README.md index 99ba15e..f2e5c18 100644 --- a/kb-app-demo-desktop/README.md +++ b/kb-app-demo-desktop/README.md @@ -1,5 +1,5 @@ - + # kb-app-demo-desktop @@ -10,7 +10,7 @@ - configuration et diagnostics de logging ; - fenêtre Wallets dédiée : inventaire sûr des identités `.kswallet`, création d’un conteneur persistant via un secret backend-only, sélection session-only du wallet d'exécution Devnet, inspection/import/export des formats keypair supportés sous `data/wallets/`, extraction sûre de pubkey depuis un keypair legacy, inspection explicite d’un `.kswallet` externe et exploration publique du solde SOL, des comptes SPL Token / Token-2022 et des transactions récentes sur un profil RPC explicitement sélectionnable ; - transport HTTP JSON-RPC et WebSocket ; -- diagnostics PostgreSQL ; +- diagnostics de stockage backend-agnostiques ; - backfill, extraction Core et replay, avec enregistrement générique de Solana Program Metadata ; - démonstrations Solana Core, SPL Memo, SPL Token, ATA et Token-2022 ; - panneau Metadata avec parcours séparés Metaplex Token Metadata, Token-2022 Token Metadata et Solana Program Metadata, préparation du profil et de la base, simulation RPC réelle, soumission explicitement confirmée et affichage des postconditions ; @@ -20,6 +20,8 @@ Les commandes Tauri sont des adaptateurs minces. La logique réutilisable appartient à `ks-pipeline`, `ks-pipeline-demo-scenarios`, `ks-lib`, `ks-store` et `ks-onchain-transport`. +Depuis `0.5.3-pre.002`, l'application conserve une façade `ks_store::Store` unique dans `AppState`. Les commandes ne construisent plus de `PostgresStore` et les payloads de configuration/diagnostic ne transportent plus de champs PostgreSQL/SQLite concrets. Les anciennes surfaces `demo_sql_*` sont renommées en `demo_store_*` dès `pre.002`; la refonte fonctionnelle finale de leurs tableaux et compteurs reste planifiée en `pre.005`. + Les campagnes Devnet/Testnet réutilisables appartiennent à `ks-pipeline-demo-scenarios`. Le desktop conserve leurs adaptateurs UI/Tauri, la sélection opérateur, la progression, les payloads TS-RS et la présentation ; `0.5.4` doit retirer les orchestrations réutilisables qui subsistent encore localement. Depuis `0.5.1-pre.008`, TS-RS appartient à cette frontière applicative : `ks-config` et `ks-lib` ne génèrent plus de bindings TypeScript. Lorsqu'un type généraliste doit être présenté au frontend, le desktop construit un DTO/wrapper dédié. diff --git a/kb-app-demo-desktop/TODO.md b/kb-app-demo-desktop/TODO.md index ab5b191..2221d12 100644 --- a/kb-app-demo-desktop/TODO.md +++ b/kb-app-demo-desktop/TODO.md @@ -1,10 +1,11 @@ - + # TODO — kb-app-demo-desktop ## Évolutions générales +- [ ] `0.5.3` — finaliser en `pre.005` la présentation des surfaces `demo_store_*`, puis aligner `demo_config` et le splash sur les modèles/compteurs du nouveau baseline sans exposer de noms physiques ; le renommage technique est terminé en `pre.002`. - [ ] `0.5.4` — réduire les panneaux d'exécution à l'adaptation UI/Tauri en déplaçant toute orchestration de campagne encore réutilisable vers `ks-pipeline-demo-scenarios`. - [ ] UX — empêcher les doubles déclenchements pendant une préparation, simulation ou soumission active. - [ ] Observabilité — afficher explicitement les comptes relus et les projections matérialisées. diff --git a/kb-app-demo-desktop/USAGE.md b/kb-app-demo-desktop/USAGE.md index 1f768f4..d948580 100644 --- a/kb-app-demo-desktop/USAGE.md +++ b/kb-app-demo-desktop/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-app-demo-desktop @@ -116,7 +116,7 @@ Les autres fenêtres sont ouvertes par les commandes Tauri dédiées : - backfill ; - HTTP ; - WebSocket ; -- diagnostics SQL ; +- diagnostics store ; - candidats replay ; - configuration ; - wallets ; @@ -311,22 +311,24 @@ L’inventaire contient cinq variantes `Create -> Mint`, puis collection `Verify Dans la colonne de résultats, `Préflight et opérations`, `Simulation et confirmation` et `Postconditions et preuves` sont présentés dans un accordéon commun. La carte `Exigences` reste sous cet accordéon et le journal d’exécution reste sous la grille, sur toute la largeur. -### Diagnostics PostgreSQL +### Diagnostics du store ```typescript const diagnostics = await invoke( - "load_demo_sql_diag" + "load_demo_store_diag" ); const rawTables = await invoke( - "load_demo_sql_pg_raw" + "load_demo_store_raw" ); const coreTables = await invoke( - "load_demo_sql_pg_core" + "load_demo_store_core" ); console.log(diagnostics, rawTables, coreTables); ``` +Les commandes/fenêtres historiques `demo_sql_*` sont renommées en `demo_store_*` dès `pre.002`, et leurs payloads sont backend-agnostiques : `connectionDescriptor`, `resources`, `backend`, `namespace` et compteurs logiques. Le frontend n'accède plus à un DSN PostgreSQL ni à des noms physiques de tables. La tranche consommateurs de `0.5.3` doit finaliser la présentation tabulaire de `demo_store_*`, aligner `demo_config` et exploiter les compteurs tables/index du nouveau baseline dans le splash. + ## Capabilities Tauri Les fenêtres autorisées sont listées dans `capabilities/default.json`. La capability active doit inclure au minimum les permissions Tauri Core et tracing nécessaires aux fenêtres déclarées. @@ -350,7 +352,7 @@ Toute nouvelle fenêtre ou commande sensible doit être auditée avant ajout de - tests de System Transfer et Memo ; - tests ATA, SPL Token et Token-2022 ; - tests de backfill, extraction Core et decode replay ; -- tests de diagnostics SQL et exports CSV ; +- tests de diagnostics store et exports CSV ; - tests de journalisation frontend ; - test du verrou d’état applicatif et de la séquence splash. @@ -368,7 +370,7 @@ Depuis la fenêtre principale, choisir **Exécution Devnet → Exécution Metada - de choisir un scénario synthétique ou un contrat de campagne Devnet ; - de sélectionner le profil Devnet ; -- de préparer et vérifier la base PostgreSQL du profil ; +- de préparer et vérifier le store persistant du profil ; - d’afficher les contrats, contrôles de préflight et preuves attendues dans le JSON viewer commun ; - de restaurer le scénario sélectionné lors de la réouverture. @@ -378,7 +380,7 @@ Les scénarios synthétiques restent disponibles pour inspecter les contrats san ### Campagnes Metaplex qualifiées -Ouvrir **Metaplex Token Metadata -> Campagnes Devnet qualifiées**, sélectionner un profil persistant et préparer PostgreSQL. Le sélecteur présente 11 campagnes : cinq familles `Create -> Mint`, collection, Print/Burn, lifecycle pNFT, escrow, maintenance et Use. Après confirmation opérateur, le desktop appelle le runner correspondant et affiche : +Ouvrir **Metaplex Token Metadata -> Campagnes Devnet qualifiées**, sélectionner un profil persistant et préparer le store du profil. Le sélecteur présente 11 campagnes : cinq familles `Create -> Mint`, collection, Print/Burn, lifecycle pNFT, escrow, maintenance et Use. Après confirmation opérateur, le desktop appelle le runner correspondant et affiche : - la fixture et son setup ; - les exécutions confirmées et les probes indisponibles dans leur ordre réel ; @@ -389,7 +391,7 @@ La surface générique par opération reste une API avancée et ne doit pas êtr ### Campagne Token-2022 Token Metadata -Ouvrir l’accordéon **Token-2022 Token Metadata**, sélectionner un profil Devnet persistant et préparer sa base PostgreSQL. Le panneau affiche le contrat fixe de cinq opérations : +Ouvrir l’accordéon **Token-2022 Token Metadata**, sélectionner un profil Devnet persistant et préparer son store persistant. Le panneau affiche le contrat fixe de cinq opérations : ```text Initialize -> UpdateField -> Emit -> RemoveKey -> UpdateAuthority diff --git a/kb-app-demo-desktop/capabilities/default.json b/kb-app-demo-desktop/capabilities/default.json index 6848785..266c761 100644 --- a/kb-app-demo-desktop/capabilities/default.json +++ b/kb-app-demo-desktop/capabilities/default.json @@ -12,10 +12,10 @@ "demo_backfill", "demo_core_extraction", "demo_decode_replay", - "demo_sql_diag", - "demo_sql_pg_raw", - "demo_sql_pg_core", - "demo_sql_replay_candidates", + "demo_store_diag", + "demo_store_raw", + "demo_store_core", + "demo_store_replay_candidates", "demo_execution_solana_core", "demo_execution_spl", "demo_execution_metadata" diff --git a/kb-app-demo-desktop/frontend/demo_core_extraction.html b/kb-app-demo-desktop/frontend/demo_core_extraction.html index fe4224a..b6b2cca 100644 --- a/kb-app-demo-desktop/frontend/demo_core_extraction.html +++ b/kb-app-demo-desktop/frontend/demo_core_extraction.html @@ -1,5 +1,5 @@ - + @@ -39,7 +39,7 @@

Extraction transactionnelle canonical vers core

-

Chaque transaction canonique sélectionnée est validée, normalisée dans les tables core et enregistrée dans le ledger de traitement au sein d'une seule transaction PostgreSQL.

+

Chaque transaction canonique sélectionnée est validée, normalisée dans les tables core et enregistrée dans le ledger de traitement au sein d'une seule transaction de stockage atomique.

@@ -163,4 +163,4 @@ - \ No newline at end of file + diff --git a/kb-app-demo-desktop/frontend/demo_decode_replay.html b/kb-app-demo-desktop/frontend/demo_decode_replay.html index ffb2a39..5a2fb44 100644 --- a/kb-app-demo-desktop/frontend/demo_decode_replay.html +++ b/kb-app-demo-desktop/frontend/demo_decode_replay.html @@ -1,5 +1,5 @@ - + @@ -61,7 +61,7 @@
- +
diff --git a/kb-app-demo-desktop/frontend/demo_sql_pg_core.html b/kb-app-demo-desktop/frontend/demo_store_core.html similarity index 80% rename from kb-app-demo-desktop/frontend/demo_sql_pg_core.html rename to kb-app-demo-desktop/frontend/demo_store_core.html index cb80fb5..636555f 100644 --- a/kb-app-demo-desktop/frontend/demo_sql_pg_core.html +++ b/kb-app-demo-desktop/frontend/demo_store_core.html @@ -1,5 +1,5 @@ - - + + @@ -8,7 +8,7 @@ - Khadhroony Bot3 — PostgreSQL core store + Khadhroony Bot3 — Core store diagnostics @@ -17,7 +17,7 @@
@@ -36,11 +36,11 @@
Statut
-
Chargement...
+
Chargement...
Profil
-
-
DSN
-
+
+
Connexion
+
@@ -49,13 +49,13 @@
-

Tables core

+

Ressources Core

- +
- - + + @@ -64,7 +64,7 @@ - +
TableDomaineRessourceModèle Rôle Existe LignesDernière insertion
@@ -75,7 +75,7 @@

Payload JSON

-
{"status":"loading","message":"Chargement des données SQL."}
+
{"status":"loading","message":"Chargement des données du store."}
@@ -91,7 +91,7 @@
- + - \ No newline at end of file + diff --git a/kb-app-demo-desktop/frontend/demo_sql_diag.html b/kb-app-demo-desktop/frontend/demo_store_diag.html similarity index 80% rename from kb-app-demo-desktop/frontend/demo_sql_diag.html rename to kb-app-demo-desktop/frontend/demo_store_diag.html index 78ad7a4..d0e33a3 100644 --- a/kb-app-demo-desktop/frontend/demo_sql_diag.html +++ b/kb-app-demo-desktop/frontend/demo_store_diag.html @@ -1,5 +1,5 @@ - - + + @@ -8,7 +8,7 @@ - Khadhroony Bot3 — SQL diagnostics + Khadhroony Bot3 — Store diagnostics @@ -17,7 +17,7 @@
@@ -31,7 +31,7 @@
-

Diagnostic PostgreSQL

+

Diagnostic du store

@@ -39,21 +39,21 @@
Statut
-
Chargement...
+
Chargement...
Fichier config
-
+
Profil
-
+
Backend
-
-
DSN
-
+
+
Connexion
+
Schema courant
-
+
Healthcheck
-
+
Migrations
-
+
@@ -62,13 +62,13 @@
-

Tables raw/core connues

+

Ressources store connues

- +
- - + + @@ -77,7 +77,7 @@ - +
TableDomaineRessourceModèle Rôle Existe LignesDernière insertion
@@ -88,7 +88,7 @@

Payload JSON

-
{"status":"loading","message":"Chargement des données SQL."}
+
{"status":"loading","message":"Chargement des données du store."}
@@ -104,7 +104,7 @@
- + - \ No newline at end of file + diff --git a/kb-app-demo-desktop/frontend/demo_sql_pg_raw.html b/kb-app-demo-desktop/frontend/demo_store_raw.html similarity index 81% rename from kb-app-demo-desktop/frontend/demo_sql_pg_raw.html rename to kb-app-demo-desktop/frontend/demo_store_raw.html index b9bbf67..68eae7a 100644 --- a/kb-app-demo-desktop/frontend/demo_sql_pg_raw.html +++ b/kb-app-demo-desktop/frontend/demo_store_raw.html @@ -1,5 +1,5 @@ - - + + @@ -8,7 +8,7 @@ - Khadhroony Bot3 — PostgreSQL canonical acquisition + Khadhroony Bot3 — Raw store diagnostics @@ -17,7 +17,7 @@
@@ -36,11 +36,11 @@
Statut
-
Chargement...
+
Chargement...
Profil
-
-
DSN
-
+
+
Connexion
+
@@ -51,11 +51,11 @@

Transactions canoniques et observations

- +
- - + + @@ -64,7 +64,7 @@ - +
TableDomaineRessourceModèle Rôle Existe LignesDernière insertion
@@ -75,7 +75,7 @@

Payload JSON

-
{"status":"loading","message":"Chargement des données SQL."}
+
{"status":"loading","message":"Chargement des données du store."}
@@ -91,7 +91,7 @@ - + - \ No newline at end of file + diff --git a/kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html b/kb-app-demo-desktop/frontend/demo_store_replay_candidates.html similarity index 97% rename from kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html rename to kb-app-demo-desktop/frontend/demo_store_replay_candidates.html index ba27886..f1010ed 100644 --- a/kb-app-demo-desktop/frontend/demo_sql_replay_candidates.html +++ b/kb-app-demo-desktop/frontend/demo_store_replay_candidates.html @@ -1,5 +1,5 @@ - - + + @@ -8,7 +8,7 @@ - Khadhroony Bot3 — Candidats de replay SQL + Khadhroony Bot3 — Candidats de replay du store @@ -17,7 +17,7 @@
Profil — @@ -36,9 +36,9 @@

Exploration read-only des candidats

-

Les requêtes sont paramétrées, bornées et sans SQL libre. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et l’export CSV utilisent toutes les lignes filtrées. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans ./data/exports_csv/. LibreOffice Calc peut toujours afficher son dialogue d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.

+

Les requêtes sont paramétrées, bornées et indépendantes du backend concret. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et l’export CSV utilisent toutes les lignes filtrées. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans ./data/exports_csv/. LibreOffice Calc peut toujours afficher son dialogue d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.

- DSN : + Connexion : Limite serveur maximale : @@ -68,7 +68,7 @@
-

Filtres PostgreSQL bornés

+

Filtres store bornés

@@ -110,8 +110,8 @@
@@ -441,7 +441,7 @@
- + - \ No newline at end of file + diff --git a/kb-app-demo-desktop/frontend/main.html b/kb-app-demo-desktop/frontend/main.html index ffcf7cd..cedaf9a 100644 --- a/kb-app-demo-desktop/frontend/main.html +++ b/kb-app-demo-desktop/frontend/main.html @@ -1,5 +1,5 @@ - + @@ -48,12 +48,12 @@
  • - +
  • -
  • SQL diagnostics
  • -
  • SQL PostgreSQL raw
  • -
  • SQL PostgreSQL core
  • -
  • SQL replay candidates
  • +
  • Store diagnostics
  • +
  • Raw store
  • +
  • Core store
  • +
  • Store replay candidates
  • @@ -100,4 +100,4 @@ - \ No newline at end of file + diff --git a/kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts b/kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts index 34d8cf6..a55b204 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts -// version: 6 +// version: 7 import * as bootstrap from "bootstrap"; import "simplebar"; @@ -127,7 +127,7 @@ async function cancelCoreExtraction(): Promise { } -async function openDiagnostics(command: "open_demo_sql_pg_raw_window" | "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window", label: string): Promise { +async function openDiagnostics(command: "open_demo_store_raw_window" | "open_demo_store_core_window" | "open_demo_store_replay_candidates_window", label: string): Promise { try { await invoke(command); appendLog({ timestamp: new Date().toISOString(), level: "info", message: `${label} ouvert.` }); @@ -166,13 +166,13 @@ document.addEventListener("DOMContentLoaded", () => { }); }); element("#openRawDiagnosticsButton").addEventListener("click", () => { - void openDiagnostics("open_demo_sql_pg_raw_window", "Diagnostic raw"); + void openDiagnostics("open_demo_store_raw_window", "Diagnostic raw"); }); element("#openCoreDiagnosticsButton").addEventListener("click", () => { - void openDiagnostics("open_demo_sql_pg_core_window", "Diagnostic core"); + void openDiagnostics("open_demo_store_core_window", "Diagnostic core"); }); element("#openReplayCandidatesButton").addEventListener("click", () => { - void openDiagnostics("open_demo_sql_replay_candidates_window", "Sélecteur de candidats replay"); + void openDiagnostics("open_demo_store_replay_candidates_window", "Sélecteur de candidats replay"); }); element("#cancelCoreExtractionButton").addEventListener("click", () => { void cancelCoreExtraction(); diff --git a/kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts b/kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts index 4696cac..fd58b95 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts -// version: 7 +// version: 8 import * as bootstrap from "bootstrap"; import "simplebar"; @@ -204,7 +204,7 @@ async function loadDiagnostics(): Promise { try { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics"); const diagnostics = await invoke("demo_decode_replay_diagnostics"); - frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed tables=${diagnostics.tables.length} coverage=${diagnostics.coverage.length}`); + frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed resources=${diagnostics.resources.length} coverage=${diagnostics.coverage.length}`); element("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); @@ -282,7 +282,7 @@ async function loadAnnotations(): Promise { } } -async function openWindow(command: "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window"): Promise { +async function openWindow(command: "open_demo_store_core_window" | "open_demo_store_replay_candidates_window"): Promise { try { frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`); await invoke(command); @@ -358,8 +358,8 @@ document.addEventListener("DOMContentLoaded", () => { element("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay()); element("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics()); element("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations()); - element("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_replay_candidates_window")); - element("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_pg_core_window")); + element("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_replay_candidates_window")); + element("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_core_window")); void listen("demo-decode-replay-progress", event => appendLog(event.payload)); void loadOptions().catch(caughtError => { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); diff --git a/kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts b/kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts index 54e1ed0..8b78991 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/frontend/ts/demo_execution_solana_core.ts -// version: 10 +// version: 11 import * as bootstrap from "bootstrap"; import "simplebar"; @@ -20,18 +20,18 @@ const logLines: string[] = []; let running = false; let profileStoreReady = false; let profileOptions: DemoExecutionSolanaCoreProfileOption[] = []; -function element(selector: string): T { const value=document.querySelector(selector); if(!value){throw new Error(`Missing UI element: ${selector}`);} return value; } -function integerValue(selector: string): number { const value=Number.parseInt(element(selector).value,10); if(!Number.isFinite(value)||value<0){throw new Error(`Valeur numérique invalide pour ${selector}`);} return value; } -function selectedProfile(): DemoExecutionSolanaCoreProfileOption|null { const name=element("#executionProfileSelect").value; return profileOptions.find(profile=>profile.name===name)??null; } -function appendLog(payload: DemoExecutionSolanaCoreProgressPayload|{timestamp:string;level:string;stage:string;message:string;signature?:string|null}):void { const signature=payload.signature?` signature=${payload.signature}`:""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`); const output=element("#executionLogOutput"); output.value=logLines.join("\n"); output.scrollTop=output.scrollHeight; } -function setRunning(value:boolean):void { running=value; const profile=selectedProfile(); element("#simulateExecutionButton").disabled=value||!profile||!profileStoreReady; element("#submitExecutionButton").disabled=value||!profile||!profileStoreReady||!profile.sendEnabled; element("#generateRecipientButton").disabled=value; element("#cancelExecutionButton").disabled=!value; const badge=element("#executionStatusBadge"); badge.textContent=value?"Exécution en cours":"Prêt"; badge.className=value?"badge text-bg-warning":"badge text-bg-success"; } -function updateProfileHelp():void { const profile=selectedProfile(); const help=element("#executionProfileHelp"); if(!profile){help.textContent="Aucun profil Devnet compatible.";return;} help.textContent=`wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled?"enabled":"disabled"}`; element("#executionAirdropInput").max=String(profile.maxAirdropLamports); element("#executionLamportsInput").max=String(profile.maxSpendLamports); setRunning(running); } -function buildRequest(submit:boolean):DemoExecutionSolanaCoreRequest { const recipient=element("#executionRecipientInput").value.trim(); if(recipient.length===0){throw new Error("Le destinataire est obligatoire.");} return {profileName:element("#executionProfileSelect").value,recipient,lamports:integerValue("#executionLamportsInput"),airdropLamports:integerValue("#executionAirdropInput"),submit,operatorConfirmed:element("#executionOperatorConfirmedInput").checked,forcePostValidationReplay:element("#executionForceReplayInput").checked,materializeAfterDecode:element("#executionMaterializeInput").checked}; } -function titledJson(title:string,value:string):string { try { const parsed:unknown=JSON.parse(value); if(parsed!==null&&typeof parsed==="object"&&!Array.isArray(parsed)){return JSON.stringify({title,...parsed},null,2);} return JSON.stringify({title,value:parsed},null,2); } catch { return JSON.stringify({title,status:"invalid_json",raw:value},null,2); } } -function displaySummary(summary:DemoExecutionSolanaCoreSummaryPayload):void { element("#executionSummaryOutput").value=JSON.stringify({title:"Résumé",...summary},null,2); element("#executionPlanOutput").value=titledJson("Plan exact",summary.planJson); element("#executionSimulationOutput").value=titledJson("Simulation exacte",summary.simulationJson); element("#executionDiagnosticsOutput").value=titledJson("Confirmation et replay",summary.diagnosticsJson); } -function displayExecutionFailure(message:string,submit:boolean):void { element("#executionSummaryOutput").value=JSON.stringify({title:"Résumé",status:"error",message},null,2); element("#executionPlanOutput").value=JSON.stringify({title:"Plan exact",status:"unavailable",reason:"preflight_failed",message:"Aucun plan exécutable n’a été produit.",error:message},null,2); element("#executionSimulationOutput").value=JSON.stringify({title:"Simulation exacte",status:"not_started",reason:"preflight_failed",message:"La simulation n’a pas été lancée.",error:message},null,2); element("#executionDiagnosticsOutput").value=JSON.stringify({title:"Confirmation et replay",status:"not_started",reason:"execution_failed_before_submission",message:submit?"La signature, l’envoi, la confirmation et le replay n’ont pas été exécutés.":"La confirmation et le replay ne s’appliquent pas à cette simulation refusée.",error:message},null,2); appendLog({timestamp:new Date().toISOString(),level:"error",stage:"failed",message}); } -async function execute(submit:boolean):Promise{if(running){return;}try{const request=buildRequest(submit);if(submit&&!request.operatorConfirmed){const message="La confirmation opérateur explicite est obligatoire avant signature et envoi.";displayExecutionFailure(message,submit);frontendWarn("kb-app-demo-desktop.frontend.demo_execution_solana_core","Submission blocked: operator confirmation missing");return;}setRunning(true);const summary=await invoke("demo_execution_solana_core_execute",{request});displaySummary(summary);}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);displayExecutionFailure(message,submit);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Execution failed: ${message}`);}finally{setRunning(false);}} +function element(selector: string): T { const value = document.querySelector(selector); if (!value) { throw new Error(`Missing UI element: ${selector}`); } return value; } +function integerValue(selector: string): number { const value = Number.parseInt(element(selector).value, 10); if (!Number.isFinite(value) || value < 0) { throw new Error(`Valeur numérique invalide pour ${selector}`); } return value; } +function selectedProfile(): DemoExecutionSolanaCoreProfileOption | null { const name = element("#executionProfileSelect").value; return profileOptions.find(profile => profile.name === name) ?? null; } +function appendLog(payload: DemoExecutionSolanaCoreProgressPayload | { timestamp: string; level: string; stage: string; message: string; signature?: string | null }): void { const signature = payload.signature ? ` signature=${payload.signature}` : ""; logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()} [${payload.stage}] ${payload.message}${signature}`); const output = element("#executionLogOutput"); output.value = logLines.join("\n"); output.scrollTop = output.scrollHeight; } +function setRunning(value: boolean): void { running = value; const profile = selectedProfile(); element("#simulateExecutionButton").disabled = value || !profile || !profileStoreReady; element("#submitExecutionButton").disabled = value || !profile || !profileStoreReady || !profile.sendEnabled; element("#generateRecipientButton").disabled = value; element("#cancelExecutionButton").disabled = !value; const badge = element("#executionStatusBadge"); badge.textContent = value ? "Exécution en cours" : "Prêt"; badge.className = value ? "badge text-bg-warning" : "badge text-bg-success"; } +function updateProfileHelp(): void { const profile = selectedProfile(); const help = element("#executionProfileHelp"); if (!profile) { help.textContent = "Aucun profil Devnet compatible."; return; } help.textContent = `wallet=${profile.walletAlias}, spendMax=${profile.maxSpendLamports}, airdropMax=${profile.maxAirdropLamports}, send=${profile.sendEnabled ? "enabled" : "disabled"}`; element("#executionAirdropInput").max = String(profile.maxAirdropLamports); element("#executionLamportsInput").max = String(profile.maxSpendLamports); setRunning(running); } +function buildRequest(submit: boolean): DemoExecutionSolanaCoreRequest { const recipient = element("#executionRecipientInput").value.trim(); if (recipient.length === 0) { throw new Error("Le destinataire est obligatoire."); } return { profileName: element("#executionProfileSelect").value, recipient, lamports: integerValue("#executionLamportsInput"), airdropLamports: integerValue("#executionAirdropInput"), submit, operatorConfirmed: element("#executionOperatorConfirmedInput").checked, forcePostValidationReplay: element("#executionForceReplayInput").checked, materializeAfterDecode: element("#executionMaterializeInput").checked }; } +function titledJson(title: string, value: string): string { try { const parsed: unknown = JSON.parse(value); if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) { return JSON.stringify({ title, ...parsed }, null, 2); } return JSON.stringify({ title, value: parsed }, null, 2); } catch { return JSON.stringify({ title, status: "invalid_json", raw: value }, null, 2); } } +function displaySummary(summary: DemoExecutionSolanaCoreSummaryPayload): void { element("#executionSummaryOutput").value = JSON.stringify({ title: "Résumé", ...summary }, null, 2); element("#executionPlanOutput").value = titledJson("Plan exact", summary.planJson); element("#executionSimulationOutput").value = titledJson("Simulation exacte", summary.simulationJson); element("#executionDiagnosticsOutput").value = titledJson("Confirmation et replay", summary.diagnosticsJson); } +function displayExecutionFailure(message: string, submit: boolean): void { element("#executionSummaryOutput").value = JSON.stringify({ title: "Résumé", status: "error", message }, null, 2); element("#executionPlanOutput").value = JSON.stringify({ title: "Plan exact", status: "unavailable", reason: "preflight_failed", message: "Aucun plan exécutable n’a été produit.", error: message }, null, 2); element("#executionSimulationOutput").value = JSON.stringify({ title: "Simulation exacte", status: "not_started", reason: "preflight_failed", message: "La simulation n’a pas été lancée.", error: message }, null, 2); element("#executionDiagnosticsOutput").value = JSON.stringify({ title: "Confirmation et replay", status: "not_started", reason: "execution_failed_before_submission", message: submit ? "La signature, l’envoi, la confirmation et le replay n’ont pas été exécutés." : "La confirmation et le replay ne s’appliquent pas à cette simulation refusée.", error: message }, null, 2); appendLog({ timestamp: new Date().toISOString(), level: "error", stage: "failed", message }); } +async function execute(submit: boolean): Promise { if (running) { return; } try { const request = buildRequest(submit); if (submit && !request.operatorConfirmed) { const message = "La confirmation opérateur explicite est obligatoire avant signature et envoi."; displayExecutionFailure(message, submit); frontendWarn("kb-app-demo-desktop.frontend.demo_execution_solana_core", "Submission blocked: operator confirmation missing"); return; } setRunning(true); const summary = await invoke("demo_execution_solana_core_execute", { request }); displaySummary(summary); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); displayExecutionFailure(message, submit); frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core", `Execution failed: ${message}`); } finally { setRunning(false); } } -async function prepareSelectedProfile():Promise{const profile=selectedProfile();profileStoreReady=false;setRunning(running);if(!profile){return;}appendLog({timestamp:new Date().toISOString(),level:"info",stage:"store_prepare",message:`Vérification PostgreSQL du profil ${profile.name}.`});try{const readiness=await invoke("demo_execution_devnet_prepare_profile",{profileName:profile.name});profileStoreReady=true;appendLog({timestamp:new Date().toISOString(),level:"info",stage:"store_ready",message:`PostgreSQL prêt: ${readiness.existingTablesAfter}/${readiness.expectedTables} tables, créées=${readiness.createdTables}, autoInit=${readiness.autoInitializeSchema}.`});}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Devnet store preparation failed: ${message}`);}finally{setRunning(running);}} -async function loadOptions():Promise{const options=await invoke("demo_execution_solana_core_options");profileOptions=options.profiles;const select=element("#executionProfileSelect");select.replaceChildren();for(const profile of profileOptions){const option=document.createElement("option");option.value=profile.name;option.textContent=profile.name;option.selected=profile.name===options.defaultProfileName;select.append(option);}element("#executionLamportsInput").value=String(options.defaultTransferLamports);updateProfileHelp();setRunning(options.running);await prepareSelectedProfile();} -document.addEventListener("DOMContentLoaded",async()=>{installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_execution_solana_core");frontendDebug("kb-app-demo-desktop.frontend.demo_execution_solana_core","Solana Core execution window loaded");await listen("demo-execution-solana-core-progress",event=>appendLog(event.payload));element("#executionProfileSelect").addEventListener("change",()=>{updateProfileHelp();void prepareSelectedProfile();});element("#generateRecipientButton").addEventListener("click",async()=>{const payload=await invoke("demo_execution_solana_core_generate_recipient");element("#executionRecipientInput").value=payload.publicKey;});element("#simulateExecutionButton").addEventListener("click",()=>void execute(false));element("#submitExecutionButton").addEventListener("click",()=>void execute(true));element("#cancelExecutionButton").addEventListener("click",()=>void invoke("demo_execution_solana_core_cancel"));element("#executionLogOutput").addEventListener("app-log-clear",()=>{logLines.length=0;});try{await loadOptions();}catch(caughtError){const message=caughtError instanceof Error?caughtError.message:String(caughtError);frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core",`Options loading failed: ${message}`);}}); +async function prepareSelectedProfile(): Promise { const profile = selectedProfile(); profileStoreReady = false; setRunning(running); if (!profile) { return; } appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification du store du profil ${profile.name}.` }); try { const readiness = await invoke("demo_execution_devnet_prepare_profile", { profileName: profile.name }); profileStoreReady = true; appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_ready", message: `Store ${readiness.backend} prêt: ${readiness.availableResourcesAfter}/${readiness.expectedResources} ressources, créées=${readiness.createdResources}, autoInit=${readiness.autoInitializeSchema}.` }); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core", `Devnet store preparation failed: ${message}`); } finally { setRunning(running); } } +async function loadOptions(): Promise { const options = await invoke("demo_execution_solana_core_options"); profileOptions = options.profiles; const select = element("#executionProfileSelect"); select.replaceChildren(); for (const profile of profileOptions) { const option = document.createElement("option"); option.value = profile.name; option.textContent = profile.name; option.selected = profile.name === options.defaultProfileName; select.append(option); } element("#executionLamportsInput").value = String(options.defaultTransferLamports); updateProfileHelp(); setRunning(options.running); await prepareSelectedProfile(); } +document.addEventListener("DOMContentLoaded", async () => { installFrontendConsoleBridge("kb-app-demo-desktop.frontend.demo_execution_solana_core"); frontendDebug("kb-app-demo-desktop.frontend.demo_execution_solana_core", "Solana Core execution window loaded"); await listen("demo-execution-solana-core-progress", event => appendLog(event.payload)); element("#executionProfileSelect").addEventListener("change", () => { updateProfileHelp(); void prepareSelectedProfile(); }); element("#generateRecipientButton").addEventListener("click", async () => { const payload = await invoke("demo_execution_solana_core_generate_recipient"); element("#executionRecipientInput").value = payload.publicKey; }); element("#simulateExecutionButton").addEventListener("click", () => void execute(false)); element("#submitExecutionButton").addEventListener("click", () => void execute(true)); element("#cancelExecutionButton").addEventListener("click", () => void invoke("demo_execution_solana_core_cancel")); element("#executionLogOutput").addEventListener("app-log-clear", () => { logLines.length = 0; }); try { await loadOptions(); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb-app-demo-desktop.frontend.demo_execution_solana_core", `Options loading failed: ${message}`); } }); diff --git a/kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts b/kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts index ad4851c..cf66cba 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts -// version: 10 +// version: 11 import * as bootstrap from "bootstrap"; import "simplebar"; @@ -896,11 +896,11 @@ async function prepareSelectedProfile(): Promise { if (!profile) { return; } - appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification PostgreSQL du profil ${profile.name}.` }); + appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification du store du profil ${profile.name}.` }); try { const readiness = await invoke("demo_execution_devnet_prepare_profile", { profileName: profile.name }); profileStoreReady = true; - appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_ready", message: `PostgreSQL prêt: ${readiness.existingTablesAfter}/${readiness.expectedTables} tables, créées=${readiness.createdTables}, autoInit=${readiness.autoInitializeSchema}.` }); + appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_ready", message: `Store ${readiness.backend} prêt: ${readiness.availableResourcesAfter}/${readiness.expectedResources} ressources, créées=${readiness.createdResources}, autoInit=${readiness.autoInitializeSchema}.` }); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Devnet store preparation failed: ${message}`); diff --git a/kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts b/kb-app-demo-desktop/frontend/ts/demo_store_core.ts similarity index 51% rename from kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts rename to kb-app-demo-desktop/frontend/ts/demo_store_core.ts index 8d4a113..07ae053 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_store_core.ts @@ -1,38 +1,38 @@ -// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts -// version: 4 +// file: kb-app-demo-desktop/frontend/ts/demo_store_core.ts +// version: 6 import * as bootstrap from "bootstrap"; import "simplebar"; import ResizeObserver from "resize-observer-polyfill"; import { invoke } from "@tauri-apps/api/core"; import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log"; -import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables"; -import type { DemoSqlPgCorePayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload"; +import { renderJsonViewer, renderTables, setText } from "./demo_store_tables"; +import type { DemoStoreCorePayload } from "./bindings/kb_app_demo_desktop/demo_store_core/DemoStoreCorePayload"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; async function refreshCore(): Promise { - setText("#sqlCoreStatus", "Chargement..."); + setText("#storeCoreStatus", "Chargement..."); try { - const payload = await invoke("load_demo_sql_pg_core"); - setText("#sqlCoreProfile", payload.activeProfileName); - setText("#sqlCoreDsn", payload.maskedDsn); - setText("#sqlCoreStatus", "OK"); - renderTables("#sqlCoreTable", "#sqlCoreTableBody", payload.tables); - renderJsonViewer("#sqlCoreJson", payload); - frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_core", "PostgreSQL core diagnostics refreshed"); + const payload = await invoke("load_demo_store_core"); + setText("#storeCoreProfile", payload.activeProfileName); + setText("#storeCoreConnection", payload.connectionDescriptor); + setText("#storeCoreStatus", "OK"); + renderTables("#storeCoreTable", "#storeCoreTableBody", payload.resources); + renderJsonViewer("#storeCoreJson", payload); + frontendDebug("kb_app_demo_desktop.frontend.demo_store_core", "Core store diagnostics refreshed"); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); - setText("#sqlCoreStatus", `Erreur : ${message}`); - renderJsonViewer("#sqlCoreJson", { error: message }); - frontendError("kb_app_demo_desktop.frontend.demo_sql_pg_core", `PostgreSQL core diagnostics loading failed: ${message}`); + setText("#storeCoreStatus", `Erreur : ${message}`); + renderJsonViewer("#storeCoreJson", { error: message }); + frontendError("kb_app_demo_desktop.frontend.demo_store_core", `Core store diagnostics loading failed: ${message}`); } } document.addEventListener("DOMContentLoaded", () => { - installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_sql_pg_core"); - frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_core", "PostgreSQL core demo window loaded"); + installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_store_core"); + frontendDebug("kb_app_demo_desktop.frontend.demo_store_core", "Core store demo window loaded"); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); document.querySelector("#refreshSqlCoreButton")?.addEventListener("click", () => { diff --git a/kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts b/kb-app-demo-desktop/frontend/ts/demo_store_diag.ts similarity index 51% rename from kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts rename to kb-app-demo-desktop/frontend/ts/demo_store_diag.ts index ff23765..c5c50ac 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_store_diag.ts @@ -1,41 +1,41 @@ -// file: kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts -// version: 3 +// file: kb-app-demo-desktop/frontend/ts/demo_store_diag.ts +// version: 5 import * as bootstrap from "bootstrap"; import "simplebar"; import ResizeObserver from "resize-observer-polyfill"; import { invoke } from "@tauri-apps/api/core"; import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log"; -import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_sql_tables"; -import type { DemoSqlDiagPayload } from "./bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload"; +import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_store_tables"; +import type { DemoStoreDiagPayload } from "./bindings/kb_app_demo_desktop/demo_store_diag/DemoStoreDiagPayload"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; -let latestPayload: DemoSqlDiagPayload | null = null; +let latestPayload: DemoStoreDiagPayload | null = null; async function refreshDiagnostics(): Promise { - setText("#sqlDiagStatus", "Chargement..."); + setText("#storeDiagStatus", "Chargement..."); try { - const payload = await invoke("load_demo_sql_diag"); - setText("#sqlDiagConfigPath", payload.configPath); - setText("#sqlDiagProfile", payload.activeProfileName); - setText("#sqlDiagBackend", payload.backend); - setText("#sqlDiagDsn", payload.maskedDsn); - setText("#sqlDiagSchema", payload.currentSchema ?? "—"); - setText("#sqlDiagHealth", payload.healthStatus); - setText("#sqlDiagMigration", payload.migrationStatus); - setText("#sqlDiagStatus", "OK"); - renderTables("#sqlDiagTable", "#sqlDiagTableBody", payload.tables); + const payload = await invoke("load_demo_store_diag"); + setText("#storeDiagConfigPath", payload.configPath); + setText("#storeDiagProfile", payload.activeProfileName); + setText("#storeDiagBackend", payload.backend); + setText("#storeDiagConnection", payload.connectionDescriptor); + setText("#storeDiagSchema", payload.namespace ?? "—"); + setText("#storeDiagHealth", payload.healthStatus); + setText("#storeDiagMigration", payload.migrationStatus); + setText("#storeDiagStatus", "OK"); + renderTables("#storeDiagTable", "#storeDiagTableBody", payload.resources); latestPayload = payload; - renderJsonViewer("#sqlDiagJson", payload); - frontendDebug("kb_app_demo_desktop.frontend.demo_sql_diag", "SQL diagnostics refreshed"); + renderJsonViewer("#storeDiagJson", payload); + frontendDebug("kb_app_demo_desktop.frontend.demo_store_diag", "Store diagnostics refreshed"); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); - setText("#sqlDiagStatus", `Erreur : ${message}`); + setText("#storeDiagStatus", `Erreur : ${message}`); latestPayload = null; - renderJsonViewer("#sqlDiagJson", { error: message }); - frontendError("kb_app_demo_desktop.frontend.demo_sql_diag", `SQL diagnostics loading failed: ${message}`); + renderJsonViewer("#storeDiagJson", { error: message }); + frontendError("kb_app_demo_desktop.frontend.demo_store_diag", `Store diagnostics loading failed: ${message}`); } } @@ -46,8 +46,8 @@ async function copyDiagnostics(): Promise { } document.addEventListener("DOMContentLoaded", () => { - installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_sql_diag"); - frontendDebug("kb_app_demo_desktop.frontend.demo_sql_diag", "SQL diagnostics demo window loaded"); + installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_store_diag"); + frontendDebug("kb_app_demo_desktop.frontend.demo_store_diag", "Store diagnostics demo window loaded"); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); document.querySelector("#refreshSqlDiagButton")?.addEventListener("click", () => { diff --git a/kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts b/kb-app-demo-desktop/frontend/ts/demo_store_raw.ts similarity index 52% rename from kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts rename to kb-app-demo-desktop/frontend/ts/demo_store_raw.ts index 6f324f7..d82f2e6 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_store_raw.ts @@ -1,38 +1,38 @@ -// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts -// version: 4 +// file: kb-app-demo-desktop/frontend/ts/demo_store_raw.ts +// version: 6 import * as bootstrap from "bootstrap"; import "simplebar"; import ResizeObserver from "resize-observer-polyfill"; import { invoke } from "@tauri-apps/api/core"; import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log"; -import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables"; -import type { DemoSqlPgRawPayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload"; +import { renderJsonViewer, renderTables, setText } from "./demo_store_tables"; +import type { DemoStoreRawPayload } from "./bindings/kb_app_demo_desktop/demo_store_raw/DemoStoreRawPayload"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; async function refreshRaw(): Promise { - setText("#sqlRawStatus", "Chargement..."); + setText("#storeRawStatus", "Chargement..."); try { - const payload = await invoke("load_demo_sql_pg_raw"); - setText("#sqlRawProfile", payload.activeProfileName); - setText("#sqlRawDsn", payload.maskedDsn); - setText("#sqlRawStatus", "OK"); - renderTables("#sqlRawTable", "#sqlRawTableBody", payload.tables); - renderJsonViewer("#sqlRawJson", payload); - frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_raw", "PostgreSQL raw diagnostics refreshed"); + const payload = await invoke("load_demo_store_raw"); + setText("#storeRawProfile", payload.activeProfileName); + setText("#storeRawConnection", payload.connectionDescriptor); + setText("#storeRawStatus", "OK"); + renderTables("#storeRawTable", "#storeRawTableBody", payload.resources); + renderJsonViewer("#storeRawJson", payload); + frontendDebug("kb_app_demo_desktop.frontend.demo_store_raw", "Raw store diagnostics refreshed"); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); - setText("#sqlRawStatus", `Erreur : ${message}`); - renderJsonViewer("#sqlRawJson", { error: message }); - frontendError("kb_app_demo_desktop.frontend.demo_sql_pg_raw", `PostgreSQL raw diagnostics loading failed: ${message}`); + setText("#storeRawStatus", `Erreur : ${message}`); + renderJsonViewer("#storeRawJson", { error: message }); + frontendError("kb_app_demo_desktop.frontend.demo_store_raw", `Raw store diagnostics loading failed: ${message}`); } } document.addEventListener("DOMContentLoaded", () => { - installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_sql_pg_raw"); - frontendDebug("kb_app_demo_desktop.frontend.demo_sql_pg_raw", "PostgreSQL raw demo window loaded"); + installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_store_raw"); + frontendDebug("kb_app_demo_desktop.frontend.demo_store_raw", "Raw store demo window loaded"); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); document.querySelector("#refreshSqlRawButton")?.addEventListener("click", () => { diff --git a/kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts b/kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts similarity index 87% rename from kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts rename to kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts index 225fb16..4b797dd 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts @@ -1,5 +1,5 @@ -// file: kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts -// version: 9 +// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts +// version: 11 import * as bootstrap from "bootstrap"; import "simplebar"; @@ -10,14 +10,14 @@ import "datatables.net-select-bs5"; import "datatables.net-select-bs5/css/select.bootstrap5.css"; import { invoke } from "@tauri-apps/api/core"; import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log"; -import type { DemoSqlReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest"; -import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow"; -import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption"; -import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload"; -import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest"; -import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow"; -import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest"; -import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow"; +import type { DemoStoreReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRequest"; +import type { DemoStoreReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRow"; +import type { DemoStoreReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayKnownProgramOption"; +import type { DemoStoreReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayOptionsPayload"; +import type { DemoStoreReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRequest"; +import type { DemoStoreReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRow"; +import type { DemoStoreReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRequest"; +import type { DemoStoreReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRow"; (window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap; (window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; @@ -40,7 +40,7 @@ type EntityViewConfig = { fileName: string; }; -const tracingTarget = "kb_app_demo_desktop.frontend.demo_sql_replay_candidates"; +const tracingTarget = "kb_app_demo_desktop.frontend.demo_store_replay_candidates"; const entityViewConfigs: EntityViewConfig[] = [ { kind: "mint", @@ -88,11 +88,11 @@ const entityViewConfigs: EntityViewConfig[] = [ fileName: "replay_account_keys.csv", }, ]; -let transactionTable: Api | null = null; -let programTable: Api | null = null; -let mintTable: Api | null = null; -let ownerTable: Api | null = null; -let accountTable: Api | null = null; +let transactionTable: Api | null = null; +let programTable: Api | null = null; +let mintTable: Api | null = null; +let ownerTable: Api | null = null; +let accountTable: Api | null = null; let busyOperationCount = 0; let maximumReplayCandidateLimit = 100_000; @@ -199,7 +199,7 @@ function badgeDisplay(value: unknown, type: string): string { return `${escapeHtml(text)}`; } -function coreStatusText(row: DemoSqlReplayTransactionRow): string { +function coreStatusText(row: DemoStoreReplayTransactionRow): string { if (!row.hasCoreTransaction) { return "absent"; } @@ -209,7 +209,7 @@ function coreStatusText(row: DemoSqlReplayTransactionRow): string { return "présent"; } -function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactionRow): string | number { +function coreDisplay(_value: unknown, type: string, row: DemoStoreReplayTransactionRow): string | number { if (type === "display") { if (!row.hasCoreTransaction) { return 'absent'; @@ -228,7 +228,7 @@ function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactio return row.hasCoreTransaction ? 1 : 2; } -function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number { +function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoStoreReplayEntityRow): string | number { const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0; if (type === "display" || type === "filter") { return ratio.toFixed(2); @@ -236,7 +236,7 @@ function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlRepla return ratio; } -function slotSpanDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number { +function slotSpanDisplay(_value: unknown, type: string, row: DemoStoreReplayEntityRow): string | number { const span = Math.max(0, row.maxSlot - row.minSlot); if (type === "display" || type === "filter") { return String(span); @@ -258,8 +258,8 @@ function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: stri }; } -function createTransactionTable(): Api { - return new DataTable("#transactionCandidateTable", { +function createTransactionTable(): Api { + return new DataTable("#transactionCandidateTable", { data: [], columns: [ { @@ -277,9 +277,9 @@ function createTransactionTable(): Api { { data: "ledgerStatus", render: badgeDisplay }, { data: "processorVersion", defaultContent: "—", render: codeDisplay }, { data: "attemptCount" }, - { data: "outerInstructionCount" }, + { data: "topLevelInstructionCount" }, { data: "innerInstructionCount" }, - { data: "outerProgramCount" }, + { data: "topLevelProgramCount" }, { data: "innerProgramCount" }, { data: "updatedAt" }, ], @@ -296,8 +296,8 @@ function createTransactionTable(): Api { }); } -function createProgramTable(): Api { - return new DataTable("#programCandidateTable", { +function createProgramTable(): Api { + return new DataTable("#programCandidateTable", { data: [], columns: [ { @@ -311,7 +311,7 @@ function createProgramTable(): Api { { data: "programCode", defaultContent: "—", render: codeDisplay }, { data: "programId", render: copyableCodeDisplay("le program ID") }, { data: "transactionCount" }, - { data: "outerInstructionCount" }, + { data: "topLevelInstructionCount" }, { data: "innerInstructionCount" }, { data: "logCount" }, { data: "minSlot" }, @@ -330,8 +330,8 @@ function createProgramTable(): Api { }); } -function createEntityTable(config: EntityViewConfig): Api { - return new DataTable(config.tableSelector, { +function createEntityTable(config: EntityViewConfig): Api { + return new DataTable(config.tableSelector, { data: [], columns: [ { @@ -363,7 +363,7 @@ function createEntityTable(config: EntityViewConfig): Api | null { +function entityTable(kind: EntityKindCode): Api | null { if (kind === "mint") { return mintTable; } @@ -425,7 +425,7 @@ function setBusy(busy: boolean, message: string): void { }); } -function buildTransactionRequest(): DemoSqlReplayTransactionRequest { +function buildTransactionRequest(): DemoStoreReplayTransactionRequest { const entityKind = optionalInputValue("#transactionEntityKindSelect"); const entityValue = optionalInputValue("#transactionEntityValueInput"); if ((entityKind === null) !== (entityValue === null)) { @@ -484,9 +484,9 @@ async function loadTransactions(): Promise { try { setBusy(true, "Chargement transactions"); const request = buildTransactionRequest(); - const rows = await invoke("load_demo_sql_replay_transactions", { request }); + const rows = await invoke("load_demo_store_replay_transactions", { request }); replaceRows(transactionTable, rows); - element("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`; + element("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis le store actif.`; frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); @@ -503,13 +503,13 @@ async function loadPrograms(): Promise { } try { setBusy(true, "Chargement programmes"); - const request: DemoSqlReplayProgramRequest = { + const request: DemoStoreReplayProgramRequest = { programIdContains: optionalInputValue("#programContainsInput"), limit: boundedPositiveIntegerValue("#programLimitInput"), }; - const rows = await invoke("load_demo_sql_replay_programs", { request }); + const rows = await invoke("load_demo_store_replay_programs", { request }); replaceRows(programTable, rows); - element("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, inner et logs liés.`; + element("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences top-level, inner et logs liés.`; frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); @@ -527,14 +527,14 @@ async function loadEntity(config: EntityViewConfig): Promise { } try { setBusy(true, `Chargement ${config.pluralLabel}`); - const request: DemoSqlReplayEntityRequest = { + const request: DemoStoreReplayEntityRequest = { entityKind: config.kind, entityValueContains: optionalInputValue(config.containsSelector), limit: boundedPositiveIntegerValue(config.limitSelector), }; - const rows = await invoke("load_demo_sql_replay_entities", { request }); + const rows = await invoke("load_demo_store_replay_entities", { request }); replaceRows(table, rows); - element(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`; + element(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis le store actif.`; frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`); } catch (caughtError) { const message = caughtError instanceof Error ? caughtError.message : String(caughtError); @@ -567,7 +567,7 @@ async function saveCsv(fileName: string, content: string, rowCount: number): Pro if (rowCount === 0) { throw new Error("Aucune ligne à exporter."); } - const path = await invoke("export_demo_sql_replay_csv", { fileName, content }); + const path = await invoke("export_demo_store_replay_csv", { fileName, content }); const badge = element("#replayCandidateStatusBadge"); badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`; badge.className = "badge text-bg-success"; @@ -577,8 +577,8 @@ async function saveCsv(fileName: string, content: string, rowCount: number): Pro async function exportTransactionsCsv(): Promise { const rows = transactionRowsForExport(); const content = csvContent( - ["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "outer_instruction_count", "inner_instruction_count", "outer_program_count", "inner_program_count", "updated_at"], - rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, row.innerProgramCount, row.updatedAt]), + ["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "top_level_instruction_count", "inner_instruction_count", "top_level_program_count", "inner_program_count", "updated_at"], + rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.topLevelInstructionCount, row.innerInstructionCount, row.topLevelProgramCount, row.innerProgramCount, row.updatedAt]), ); await saveCsv("replay_transactions.csv", content, rows.length); } @@ -586,8 +586,8 @@ async function exportTransactionsCsv(): Promise { async function exportProgramsCsv(): Promise { const rows = programRowsForExport(); const content = csvContent( - ["program_code", "program_id", "transaction_count", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"], - rows.map(row => [row.programCode, row.programId, row.transactionCount, row.outerInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]), + ["program_code", "program_id", "transaction_count", "top_level_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"], + rows.map(row => [row.programCode, row.programId, row.transactionCount, row.topLevelInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]), ); await saveCsv("replay_programs.csv", content, rows.length); } @@ -601,21 +601,21 @@ async function exportEntityCsv(config: EntityViewConfig): Promise { await saveCsv(config.fileName, content, rows.length); } -function transactionRowsForExport(): DemoSqlReplayTransactionRow[] { +function transactionRowsForExport(): DemoStoreReplayTransactionRow[] { if (transactionTable === null) { return []; } return selectedOrFilteredRows(transactionTable); } -function programRowsForExport(): DemoSqlReplayProgramRow[] { +function programRowsForExport(): DemoStoreReplayProgramRow[] { if (programTable === null) { return []; } return selectedOrFilteredRows(programTable); } -function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] { +function entityRowsForExport(kind: EntityKindCode): DemoStoreReplayEntityRow[] { const table = entityTable(kind); if (table === null) { return []; @@ -660,13 +660,13 @@ async function openCoreExtraction(): Promise { } async function loadOptions(): Promise { - const options = await invoke("demo_sql_replay_options"); + const options = await invoke("demo_store_replay_options"); if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) { throw new Error("La limite maximale des candidats replay retournée par le backend est invalide."); } maximumReplayCandidateLimit = options.maximumLimit; element("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`; - element("#replayCandidateDsn").textContent = options.maskedDsn; + element("#replayCandidateDsn").textContent = options.connectionDescriptor; element("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit); document.querySelectorAll("input[type=number][max]").forEach(input => { input.max = String(options.maximumLimit); @@ -674,7 +674,7 @@ async function loadOptions(): Promise { populateKnownPrograms(options.knownPrograms); } -function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): void { +function populateKnownPrograms(programs: DemoStoreReplayKnownProgramOption[]): void { const select = element("#knownProgramSelect"); const datalist = element("#knownProgramDatalist"); select.replaceChildren(new Option("Tous les programmes connus", "")); @@ -779,7 +779,7 @@ function installHandlers(): void { document.addEventListener("DOMContentLoaded", () => { installFrontendConsoleBridge(tracingTarget); - frontendDebug(tracingTarget, "SQL replay candidate browser loaded"); + frontendDebug(tracingTarget, "Store replay candidate browser loaded"); transactionTable = createTransactionTable(); programTable = createProgramTable(); mintTable = createEntityTable(entityViewConfigs[0]); diff --git a/kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts b/kb-app-demo-desktop/frontend/ts/demo_store_tables.ts similarity index 58% rename from kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts rename to kb-app-demo-desktop/frontend/ts/demo_store_tables.ts index ae25c93..5739acb 100644 --- a/kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts +++ b/kb-app-demo-desktop/frontend/ts/demo_store_tables.ts @@ -1,10 +1,10 @@ -// file: kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts -// version: 4 +// file: kb-app-demo-desktop/frontend/ts/demo_store_tables.ts +// version: 6 import DataTable, { type Api } from "datatables.net-bs5"; import "datatables.net-bs5/css/dataTables.bootstrap5.css"; import { jsonText, renderJsonViewer } from "./json_viewer.ts"; -import type { DemoSqlTableSnapshot } from "./bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot"; +import type { DemoStoreResourceSnapshot } from "./bindings/kb_app_demo_desktop/demo_store/DemoStoreResourceSnapshot"; const tableInstances = new Map(); @@ -15,7 +15,7 @@ export function setText(selector: string, value: string): void { } } -export function renderTables(tableSelector: string, bodySelector: string, tables: DemoSqlTableSnapshot[]): void { +export function renderTables(tableSelector: string, bodySelector: string, resources: DemoStoreResourceSnapshot[]): void { const existing = tableInstances.get(tableSelector); if (existing) { existing.destroy(); @@ -26,16 +26,16 @@ export function renderTables(tableSelector: string, bodySelector: string, tables return; } tbody.textContent = ""; - for (const table of tables) { + for (const resource of resources) { const row = document.createElement("tr"); - row.appendChild(cell(table.tableName, true)); - row.appendChild(cell(table.domain, false)); - row.appendChild(cell(table.role, false)); - row.appendChild(cell(table.exists ? "oui" : "non", false)); - row.appendChild(cell(table.rowCount === null ? "—" : String(table.rowCount), false)); - row.appendChild(cell(table.minSlot === null ? "—" : String(table.minSlot), false)); - row.appendChild(cell(table.maxSlot === null ? "—" : String(table.maxSlot), false)); - row.appendChild(cell(table.latestCreatedAt ?? "—", false)); + row.appendChild(cell(resource.resourceCode, true)); + row.appendChild(cell(resource.modelCode, false)); + row.appendChild(cell(resource.role, false)); + row.appendChild(cell(resource.available ? "oui" : "non", false)); + row.appendChild(cell(resource.recordCount === null ? "—" : String(resource.recordCount), false)); + row.appendChild(cell(resource.minSlot === null ? "—" : String(resource.minSlot), false)); + row.appendChild(cell(resource.maxSlot === null ? "—" : String(resource.maxSlot), false)); + row.appendChild(cell(resource.latestCreatedAt ?? "—", false)); tbody.appendChild(row); } const tableElement = document.querySelector(tableSelector); @@ -48,12 +48,12 @@ export function renderTables(tableSelector: string, bodySelector: string, tables lengthMenu: [10, 25, 50, 100], order: [[0, "asc"]], language: { - emptyTable: "Aucune table disponible", - info: "Tables _START_ à _END_ sur _TOTAL_", - infoEmpty: "Aucune table", - lengthMenu: "Afficher _MENU_ tables", + emptyTable: "Aucune ressource disponible", + info: "Ressources _START_ à _END_ sur _TOTAL_", + infoEmpty: "Aucune ressource", + lengthMenu: "Afficher _MENU_ ressources", search: "Rechercher :", - zeroRecords: "Aucune table correspondante", + zeroRecords: "Aucune ressource correspondante", }, })); } diff --git a/kb-app-demo-desktop/frontend/ts/main.ts b/kb-app-demo-desktop/frontend/ts/main.ts index af2f019..1574908 100644 --- a/kb-app-demo-desktop/frontend/ts/main.ts +++ b/kb-app-demo-desktop/frontend/ts/main.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/frontend/ts/main.ts -// version: 7 +// version: 8 import * as bootstrap from "bootstrap"; import MarkdownIt from "markdown-it"; @@ -97,10 +97,10 @@ for (const [elementId, command] of pipelineDemoLinks) { } const sqlDemoLinks: ReadonlyArray = [ - ["openDemoSqlDiagLink", "open_demo_sql_diag_window"], - ["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"], - ["openDemoSqlPgCoreLink", "open_demo_sql_pg_core_window"], - ["openDemoSqlReplayCandidatesLink", "open_demo_sql_replay_candidates_window"], + ["openDemoStoreDiagLink", "open_demo_store_diag_window"], + ["openDemoStoreRawLink", "open_demo_store_raw_window"], + ["openDemoStoreCoreLink", "open_demo_store_core_window"], + ["openDemoStoreReplayCandidatesLink", "open_demo_store_replay_candidates_window"], ]; for (const [elementId, command] of sqlDemoLinks) { const link = document.querySelector(`#${elementId}`); diff --git a/kb-app-demo-desktop/package.json b/kb-app-demo-desktop/package.json index 0242092..b4428af 100644 --- a/kb-app-demo-desktop/package.json +++ b/kb-app-demo-desktop/package.json @@ -1,7 +1,7 @@ { "name": "kb-app-demo-desktop", "private": true, - "version": "0.5.2", + "version": "0.5.3", "type": "module", "scripts": { "dev": "vite", diff --git a/kb-app-demo-desktop/src/app_state.rs b/kb-app-demo-desktop/src/app_state.rs index c47cb1b..f53180a 100644 --- a/kb-app-demo-desktop/src/app_state.rs +++ b/kb-app-demo-desktop/src/app_state.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/app_state.rs -// version: 17 +// version: 20 //! Shared Tauri application state and startup initialization. @@ -17,6 +17,7 @@ pub(crate) struct AppState { active_profile: ks_config::ProfileConfig, logging_guard: std::sync::Mutex, http_pool: ks_onchain_transport::HttpEndpointPool, + store: tokio::sync::OnceCell, ws_pool: std::sync::Mutex>>, demo_ws_session: @@ -106,6 +107,7 @@ impl crate::AppState { active_profile, logging_guard: std::sync::Mutex::new(logging_guard), http_pool, + store: tokio::sync::OnceCell::new(), ws_pool: std::sync::Mutex::new(std::option::Option::None), demo_ws_session: tokio::sync::Mutex::new(std::option::Option::None), demo_backfill_running: std::sync::atomic::AtomicBool::new(false), @@ -174,6 +176,25 @@ impl crate::AppState { return &self.active_profile; } + /// Returns the persistent backend-agnostic store opened from the active profile. + pub(crate) async fn store(&self) -> ks_core::Result<&ks_store::Store> { + if let std::option::Option::Some(store) = self.store.get() { + return std::result::Result::Ok(store); + } + let options = match ks_store::StoreOpenOptions::new( + self.active_profile.database.enabled, + self.active_profile.database.backend.clone(), + self.active_profile.database.backend_options.clone(), + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return self + .store + .get_or_try_init(|| return async move { return ks_store::Store::open(options).await }) + .await; + } + /// Returns the session-only execution-wallet alias override for one profile. pub(crate) fn demo_execution_wallet_alias_override( &self, diff --git a/kb-app-demo-desktop/src/demo_backfill.rs b/kb-app-demo-desktop/src/demo_backfill.rs index cb9910d..1ec4bb2 100644 --- a/kb-app-demo-desktop/src/demo_backfill.rs +++ b/kb-app-demo-desktop/src/demo_backfill.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_backfill.rs -// version: 16 +// version: 17 //! Tauri commands and UI payloads for bounded HTTP transaction backfills. @@ -288,23 +288,10 @@ pub(crate) async fn demo_backfill_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let profile = state.active_profile(); - let store_options = match ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) { + let store = match crate::shared_store(state.inner()).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let store = match ks_store::PostgresStore::connect(store_options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let observer = crate::DemoBackfillObserver { app_handle, cancel_requested: state.demo_backfill_cancel_requested(), diff --git a/kb-app-demo-desktop/src/demo_config.rs b/kb-app-demo-desktop/src/demo_config.rs index 26bf2a6..9791ba0 100644 --- a/kb-app-demo-desktop/src/demo_config.rs +++ b/kb-app-demo-desktop/src/demo_config.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_config.rs -// version: 6 +// version: 8 //! Configuration demo payloads with explicit public and bounded diagnostic projections. @@ -164,7 +164,7 @@ pub(crate) struct DemoConfigEndpointDiagnosticPayload { pub(crate) role_count: u32, } -/// Store diagnostic projection without DSN or SQLite path content. +/// Backend-agnostic store diagnostic projection without backend option values. #[derive(Clone, Debug, serde::Serialize, TS)] #[ts( export, @@ -173,23 +173,16 @@ pub(crate) struct DemoConfigEndpointDiagnosticPayload { pub(crate) struct DemoConfigStoreDiagnosticPayload { /// Whether persistence is enabled. pub(crate) enabled: bool, - /// Selected store backend. + /// Selected store backend code. pub(crate) backend: std::string::String, - /// Whether a PostgreSQL URL is configured. - pub(crate) postgres_url_state: std::string::String, - /// PostgreSQL connection pool ceiling. - pub(crate) postgres_max_connections: u32, - /// PostgreSQL connection timeout in milliseconds. - #[ts(type = "number")] - pub(crate) postgres_connect_timeout_ms: u64, - /// Whether PostgreSQL schema initialization is enabled. - pub(crate) postgres_auto_initialize_schema: bool, - /// Whether a SQLite path is configured. - pub(crate) sqlite_path_state: std::string::String, - /// SQLite connection pool ceiling. - pub(crate) sqlite_max_connections: u32, - /// Whether SQLite schema initialization is enabled. - pub(crate) sqlite_auto_initialize_schema: bool, + /// Whether backend-specific configuration validated successfully. + pub(crate) configuration_status: std::string::String, + /// Whether a backend connection or location value is configured. + pub(crate) connection_state: std::string::String, + /// Whether automatic store initialization is enabled. + pub(crate) auto_initialize_schema: bool, + /// Number of opaque backend options supplied after composition. + pub(crate) backend_option_count: u32, } /// Wallet diagnostic projection without paths or secret key material. @@ -375,17 +368,7 @@ fn diagnostic_config_payload(state: &crate::AppState) -> DemoConfigDiagnosticPay source_diagnostic("execution", state.execution_config_path()), ], endpoints, - store: DemoConfigStoreDiagnosticPayload { - enabled: profile.database.enabled, - backend: profile.database.backend.clone(), - postgres_url_state: configured_state(profile.database.postgres.url.as_str()), - postgres_max_connections: profile.database.postgres.max_connections, - postgres_connect_timeout_ms: profile.database.postgres.connect_timeout_ms, - postgres_auto_initialize_schema: profile.database.postgres.auto_initialize_schema, - sqlite_path_state: configured_state(profile.database.sqlite.path.as_str()), - sqlite_max_connections: profile.database.sqlite.max_connections, - sqlite_auto_initialize_schema: profile.database.sqlite.auto_initialize_schema, - }, + store: store_diagnostic(profile), wallet: DemoConfigWalletDiagnosticPayload { wallet_directory_state: configured_state(profile.wallet.wallet_dir.as_str()), cluster: profile.wallet.cluster.clone(), @@ -398,6 +381,55 @@ fn diagnostic_config_payload(state: &crate::AppState) -> DemoConfigDiagnosticPay }; } +fn store_diagnostic(profile: &ks_config::ProfileConfig) -> DemoConfigStoreDiagnosticPayload { + let backend_option_count = match profile.database.backend_options.as_object() { + std::option::Option::Some(value) => bounded_len(value.len()), + std::option::Option::None => 0, + }; + let options = match ks_store::StoreOpenOptions::new( + profile.database.enabled, + profile.database.backend.clone(), + profile.database.backend_options.clone(), + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return DemoConfigStoreDiagnosticPayload { + enabled: profile.database.enabled, + backend: profile.database.backend.clone(), + configuration_status: "invalid".to_string(), + connection_state: "unknown".to_string(), + auto_initialize_schema: false, + backend_option_count, + }; + }, + }; + let summary = match options.configuration_summary() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return DemoConfigStoreDiagnosticPayload { + enabled: profile.database.enabled, + backend: profile.database.backend.clone(), + configuration_status: "invalid".to_string(), + connection_state: "unknown".to_string(), + auto_initialize_schema: false, + backend_option_count, + }; + }, + }; + return DemoConfigStoreDiagnosticPayload { + enabled: summary.enabled, + backend: summary.backend_code, + configuration_status: "ready".to_string(), + connection_state: if summary.connection_configured { + "configured".to_string() + } else { + "missing".to_string() + }, + auto_initialize_schema: summary.auto_initialize_schema, + backend_option_count: summary.backend_option_count, + }; +} + fn source_diagnostic(category: &str, path: &str) -> DemoConfigSourceDiagnosticPayload { return DemoConfigSourceDiagnosticPayload { category: category.to_string(), @@ -503,9 +535,19 @@ mod tests { #[test] fn public_and_diagnostic_projections_never_serialize_secret_canaries() { let mut profile = active_fixture_profile(); - profile.database.postgres.url = - "postgres://operator:POSTGRES-SECRET-CANARY@localhost/solana".to_string(); - profile.database.sqlite.path = "/private/SQLITE-PATH-CANARY.sqlite".to_string(); + if let std::option::Option::Some(options) = profile.database.backend_options.as_object_mut() + { + options.insert( + "url".to_string(), + serde_json::Value::String( + "postgres://operator:POSTGRES-SECRET-CANARY@localhost/solana".to_string(), + ), + ); + options.insert( + "private_path".to_string(), + serde_json::Value::String("/private/STORE-PATH-CANARY".to_string()), + ); + } profile.wallet.wallet_dir = "/private/WALLET-PATH-CANARY".to_string(); if let std::option::Option::Some(endpoint) = profile.solana.http_endpoints.first_mut() { endpoint.url = "https://provider.invalid/?api-key=HELIUS-SECRET-CANARY".to_string(); @@ -536,17 +578,7 @@ mod tests { }; }) .collect::>(), - store: super::DemoConfigStoreDiagnosticPayload { - enabled: profile.database.enabled, - backend: profile.database.backend.clone(), - postgres_url_state: super::configured_state(profile.database.postgres.url.as_str()), - postgres_max_connections: profile.database.postgres.max_connections, - postgres_connect_timeout_ms: profile.database.postgres.connect_timeout_ms, - postgres_auto_initialize_schema: profile.database.postgres.auto_initialize_schema, - sqlite_path_state: super::configured_state(profile.database.sqlite.path.as_str()), - sqlite_max_connections: profile.database.sqlite.max_connections, - sqlite_auto_initialize_schema: profile.database.sqlite.auto_initialize_schema, - }, + store: super::store_diagnostic(&profile), wallet: super::DemoConfigWalletDiagnosticPayload { wallet_directory_state: super::configured_state(profile.wallet.wallet_dir.as_str()), cluster: profile.wallet.cluster.clone(), @@ -563,7 +595,7 @@ mod tests { }; for secret in [ "POSTGRES-SECRET-CANARY", - "SQLITE-PATH-CANARY", + "STORE-PATH-CANARY", "WALLET-PATH-CANARY", "HELIUS-SECRET-CANARY", "WS-SECRET-CANARY", diff --git a/kb-app-demo-desktop/src/demo_core_extraction.rs b/kb-app-demo-desktop/src/demo_core_extraction.rs index 6933d12..afd2eaa 100644 --- a/kb-app-demo-desktop/src/demo_core_extraction.rs +++ b/kb-app-demo-desktop/src/demo_core_extraction.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_core_extraction.rs -// version: 14 +// version: 17 //! Tauri commands and UI payloads for canonical transaction to core extraction. @@ -221,7 +221,7 @@ pub(crate) async fn demo_core_extraction_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/kb-app-demo-desktop/src/demo_decode_replay.rs b/kb-app-demo-desktop/src/demo_decode_replay.rs index 8708ff2..fd1dd8f 100644 --- a/kb-app-demo-desktop/src/demo_decode_replay.rs +++ b/kb-app-demo-desktop/src/demo_decode_replay.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_decode_replay.rs -// version: 40 +// version: 44 //! Tauri commands and UI payloads for contextual instruction decode replay. @@ -255,8 +255,8 @@ pub(crate) struct DemoDecodeCoverageSummaryPayload { export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts" )] pub(crate) struct DemoDecodeDiagnosticsPayload { - /// Decode, materialization and ledger table diagnostics. - pub(crate) tables: std::vec::Vec, + /// Decode, materialization and ledger resource diagnostics. + pub(crate) resources: std::vec::Vec, /// Aggregated declared and observed coverage. pub(crate) coverage: std::vec::Vec, } @@ -495,7 +495,7 @@ pub(crate) async fn demo_decode_replay_execute( materialize_after_decode = pipeline_request.materialize_after_decode, "start contextual decode replay pipeline" ); - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state.inner()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -563,24 +563,24 @@ pub(crate) async fn demo_decode_replay_diagnostics( state: tauri::State<'_, crate::AppState>, ) -> std::result::Result { tracing::debug!(target: crate::TRACING_TARGET, action = "load_diagnostics", coverage_limit = 500_u32, "load contextual decode replay diagnostics"); - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state.inner()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let table_result = store.known_table_diagnostics().await; - let all_tables = match table_result { + let resource_result = store.known_resource_diagnostics().await; + let all_resources = match resource_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let selected_tables: std::vec::Vec = all_tables + let selected_resources: std::vec::Vec = all_resources .iter() - .filter(|table| { - return table.table_name.starts_with("kb_sol_decode_") - || table.table_name.starts_with("kb_sol_mat_") - || table.table_name == "kb_sol_ops_processing_ledger"; + .filter(|resource| { + return resource.model_code == "processing" + || resource.model_code == "decode" + || resource.model_code == "materialization"; }) - .map(crate::table_snapshot_from_pg) + .map(crate::store_resource_snapshot) .collect(); let coverage_result = ks_store::DecodePipelineStore::list_decode_coverage_summary( &store, @@ -593,9 +593,9 @@ pub(crate) async fn demo_decode_replay_diagnostics( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - tracing::debug!(target: crate::TRACING_TARGET, action = "load_diagnostics", table_count = selected_tables.len(), coverage_count = coverage.len(), "contextual decode replay diagnostics loaded"); + tracing::debug!(target: crate::TRACING_TARGET, action = "load_diagnostics", resource_count = selected_resources.len(), coverage_count = coverage.len(), "contextual decode replay diagnostics loaded"); return std::result::Result::Ok(crate::DemoDecodeDiagnosticsPayload { - tables: selected_tables, + resources: selected_resources, coverage: coverage.into_iter().map(coverage_payload).collect(), }); } @@ -616,7 +616,7 @@ pub(crate) async fn demo_decode_replay_annotations( std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; tracing::debug!(target: crate::TRACING_TARGET, action = "load_transaction_annotations", signature_contains = ?filter.signature_contains, limit = filter.limit, "load bounded committed transaction annotation journal"); - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state.inner()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/kb-app-demo-desktop/src/demo_devnet_common.rs b/kb-app-demo-desktop/src/demo_devnet_common.rs index f14246a..8a99585 100644 --- a/kb-app-demo-desktop/src/demo_devnet_common.rs +++ b/kb-app-demo-desktop/src/demo_devnet_common.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_devnet_common.rs -// version: 6 +// version: 7 //! Shared Devnet demo UI contracts. @@ -7,7 +7,7 @@ use ts_rs::TS; // rust-rules: derive-import const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD"; -/// Readiness report for one Devnet profile PostgreSQL store. +/// Backend-agnostic readiness report for one Devnet profile store. #[derive(Clone, Debug, serde::Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts( @@ -17,16 +17,18 @@ const DEMO_WALLET_PASSWORD_ENV: &str = "KB_SECRET_DEMO_WALLET_PASSWORD"; pub(crate) struct DemoExecutionDevnetStoreReadinessPayload { /// Selected Devnet profile. pub(crate) profile_name: std::string::String, + /// Selected backend code. + pub(crate) backend: std::string::String, /// Whether schema creation was allowed by configuration. pub(crate) auto_initialize_schema: bool, - /// Number of known tables already present before preparation. - pub(crate) existing_tables_before: u32, - /// Number of known tables created during preparation. - pub(crate) created_tables: u32, - /// Number of known tables present after preparation. - pub(crate) existing_tables_after: u32, - /// Total number of known tables expected by the current store. - pub(crate) expected_tables: u32, + /// Number of logical resources already present before preparation. + pub(crate) available_resources_before: u32, + /// Number of logical resources created during preparation. + pub(crate) created_resources: u32, + /// Number of logical resources present after preparation. + pub(crate) available_resources_after: u32, + /// Total number of logical resources expected by the current store contract. + pub(crate) expected_resources: u32, } /// Converts the reusable scenario readiness report to the desktop TS-RS payload. @@ -35,11 +37,12 @@ pub(crate) fn devnet_store_readiness_payload( ) -> DemoExecutionDevnetStoreReadinessPayload { return DemoExecutionDevnetStoreReadinessPayload { profile_name: readiness.profile_name, + backend: readiness.backend, auto_initialize_schema: readiness.auto_initialize_schema, - existing_tables_before: bounded_u32(readiness.existing_tables_before), - created_tables: bounded_u32(readiness.created_tables), - existing_tables_after: bounded_u32(readiness.existing_tables_after), - expected_tables: bounded_u32(readiness.expected_tables), + available_resources_before: bounded_u32(readiness.available_resources_before), + created_resources: bounded_u32(readiness.created_resources), + available_resources_after: bounded_u32(readiness.available_resources_after), + expected_resources: bounded_u32(readiness.expected_resources), }; } diff --git a/kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs b/kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs index 5b4efe7..e24839f 100644 --- a/kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs +++ b/kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_execution_metadata_metaplex_token_metadata.rs -// version: 11 +// version: 13 //! Desktop adapters for generic and qualified Metaplex Token Metadata execution workflows. @@ -190,13 +190,10 @@ pub(crate) async fn demo_execution_metadata_metaplex_token_metadata_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let mut execution_request = match ks_pipeline_demo_scenarios::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( request.intent_id, request.operation_json.as_str(), @@ -1044,13 +1041,10 @@ pub(crate) async fn demo_execution_metadata_metaplex_execute_campaign( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let workspace_root = crate::workspace_root_dir(); let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); diff --git a/kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs b/kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs index 0f32c41..0313d9e 100644 --- a/kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs +++ b/kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_execution_metadata_token_2022.rs -// version: 5 +// version: 7 //! Thin desktop adapter for the complete Token-2022 Token Metadata Devnet campaign. @@ -88,13 +88,10 @@ pub(crate) async fn demo_execution_metadata_token_2022_execute_campaign( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let workspace_root = crate::workspace_root_dir(); let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); diff --git a/kb-app-demo-desktop/src/demo_execution_solana_core.rs b/kb-app-demo-desktop/src/demo_execution_solana_core.rs index 0eadbb7..9ff530d 100644 --- a/kb-app-demo-desktop/src/demo_execution_solana_core.rs +++ b/kb-app-demo-desktop/src/demo_execution_solana_core.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_execution_solana_core.rs -// version: 20 +// version: 23 //! Tauri adapter for bounded Solana Core execution on Devnet. @@ -394,7 +394,7 @@ pub(crate) fn confirmation_status_code( }; } -/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile. +/// Verifies or initializes the configured store selected by one Devnet profile. pub(crate) async fn demo_execution_devnet_prepare_profile( state: tauri::State<'_, crate::AppState>, profile_name: std::string::String, @@ -473,14 +473,10 @@ pub(crate) async fn demo_execution_solana_core_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let initialize_result = store.initialize_store_schema().await; - if let std::result::Result::Err(error) = initialize_result { - return std::result::Result::Err(error.to_string()); - } let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetSystemTransferRequest::new( format!("demo-execution-{}", chrono::Utc::now().timestamp_micros()), ks_lib::MdPubkey(request.recipient.trim().to_string()), @@ -561,13 +557,10 @@ pub(crate) async fn demo_execution_spl_memo_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetMemoExecutionRequest::new( format!("demo-memo-execution-{}", chrono::Utc::now().timestamp_micros()), request.message, diff --git a/kb-app-demo-desktop/src/demo_spl_ata.rs b/kb-app-demo-desktop/src/demo_spl_ata.rs index 758b295..412685a 100644 --- a/kb-app-demo-desktop/src/demo_spl_ata.rs +++ b/kb-app-demo-desktop/src/demo_spl_ata.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_spl_ata.rs -// version: 16 +// version: 18 //! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads. @@ -260,13 +260,10 @@ pub(crate) async fn demo_execution_spl_ata_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let decoders: std::vec::Vec> = std::vec![ std::sync::Arc::new(ks_lib::DcSplAssociatedTokenAccountDecoder), std::sync::Arc::new(ks_lib::DcSplTokenDecoder), @@ -315,7 +312,7 @@ pub(crate) async fn demo_spl_ata_journal( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; diff --git a/kb-app-demo-desktop/src/demo_spl_token.rs b/kb-app-demo-desktop/src/demo_spl_token.rs index 39d36d0..0a0eaa2 100644 --- a/kb-app-demo-desktop/src/demo_spl_token.rs +++ b/kb-app-demo-desktop/src/demo_spl_token.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_spl_token.rs -// version: 13 +// version: 16 //! Thin Tauri adapter for classic SPL Token execution and materialized journals. @@ -99,7 +99,7 @@ pub(crate) struct DemoExecutionSplTokenSummaryPayload { export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts" )] pub(crate) struct DemoSplTokenJournalRequest { - /// Devnet profile whose PostgreSQL store is queried. + /// Devnet profile whose configured store is queried. pub(crate) profile_name: std::string::String, /// Optional partial signature handled by the bounded store query. pub(crate) signature_contains: std::option::Option, @@ -183,13 +183,10 @@ pub(crate) async fn demo_execution_spl_token_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let amount_raw = request.amount_raw.trim().to_string(); let operation = ks_lib::ExSplClassicTokenOperation::Instruction { value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { @@ -259,13 +256,10 @@ pub(crate) async fn demo_spl_token_journal( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let needs_payload_filter = validated.mint.is_some() || validated.account.is_some() || validated.operation.is_some(); let query_limit = if needs_payload_filter { diff --git a/kb-app-demo-desktop/src/demo_spl_token_2022.rs b/kb-app-demo-desktop/src/demo_spl_token_2022.rs index eab2bf3..c383322 100644 --- a/kb-app-demo-desktop/src/demo_spl_token_2022.rs +++ b/kb-app-demo-desktop/src/demo_spl_token_2022.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_spl_token_2022.rs -// version: 18 +// version: 20 //! Thin Tauri adapter for independent public Token-2022 Devnet validation scenarios. @@ -159,13 +159,10 @@ pub(crate) async fn demo_execution_spl_token_2022_execute( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store = match crate::connect_postgres_store(&profile).await { + let store = match crate::open_store(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - return std::result::Result::Err(error.to_string()); - } let operation = match operation_from_request(&request) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/kb-app-demo-desktop/src/demo_sql_common.rs b/kb-app-demo-desktop/src/demo_sql_common.rs deleted file mode 100644 index cda9a48..0000000 --- a/kb-app-demo-desktop/src/demo_sql_common.rs +++ /dev/null @@ -1,312 +0,0 @@ -// file: kb-app-demo-desktop/src/demo_sql_common.rs -// version: 13 - -//! Shared SQL demo helpers and serializable payloads. - -use ts_rs::TS; // rust-rules: derive-import - -/// UI-safe table diagnostics shown by SQL demo windows. -#[derive(Clone, Debug, serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts( - export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot.ts" -)] -pub(crate) struct DemoSqlTableSnapshot { - /// Table name. - pub(crate) table_name: std::string::String, - /// Logical Solana domain encoded in the table name. - pub(crate) domain: std::string::String, - /// Human-readable table role. - pub(crate) role: std::string::String, - /// Whether the table currently exists. - pub(crate) exists: bool, - /// Row count when the table exists. - #[ts(type = "number | null")] - pub(crate) row_count: std::option::Option, - /// Minimum slot when available. - #[ts(type = "number | null")] - pub(crate) min_slot: std::option::Option, - /// Maximum slot when available. - #[ts(type = "number | null")] - pub(crate) max_slot: std::option::Option, - /// Latest insertion timestamp rendered by PostgreSQL when available. - pub(crate) latest_created_at: std::option::Option, -} - -/// Builds PostgreSQL options from the active profile without coupling ks-store to ks-config. -pub(crate) fn postgres_store_options_from_profile( - profile: &ks_config::ProfileConfig, - auto_initialize_schema: bool, -) -> ks_core::Result { - return ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - auto_initialize_schema, - ); -} - -/// Connects to PostgreSQL without reapplying schemas initialized at application startup. -pub(crate) async fn connect_postgres_store( - profile: &ks_config::ProfileConfig, -) -> std::result::Result { - if !profile.database.enabled { - return std::result::Result::Err(std::string::String::from( - "database configuration is disabled in the active profile", - )); - } - let options_result = postgres_store_options_from_profile(profile, false); - let mut options = match options_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - }; - options.auto_initialize_schema = false; - let store_result = ks_store::PostgresStore::connect(options).await; - return match store_result { - std::result::Result::Ok(store) => std::result::Result::Ok(store), - std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), - }; -} - -/// Converts store table diagnostics to the UI payload shape. -pub(crate) fn table_snapshot_from_pg( - value: &ks_store::PostgresTableDiagnostics, -) -> crate::DemoSqlTableSnapshot { - let row_count = match &value.statistics { - std::option::Option::Some(statistics) => std::option::Option::Some(statistics.row_count), - std::option::Option::None => std::option::Option::None, - }; - let min_slot = match &value.statistics { - std::option::Option::Some(statistics) => statistics.min_slot, - std::option::Option::None => std::option::Option::None, - }; - let max_slot = match &value.statistics { - std::option::Option::Some(statistics) => statistics.max_slot, - std::option::Option::None => std::option::Option::None, - }; - let latest_created_at = match &value.statistics { - std::option::Option::Some(statistics) => statistics.latest_created_at.clone(), - std::option::Option::None => std::option::Option::None, - }; - return crate::DemoSqlTableSnapshot { - table_name: value.table_name.clone(), - domain: value.domain.clone(), - role: value.role.clone(), - exists: value.exists, - row_count, - min_slot, - max_slot, - latest_created_at, - }; -} - -/// Converts many table diagnostics to UI payloads. -pub(crate) fn table_snapshots_from_pg( - values: &[ks_store::PostgresTableDiagnostics], -) -> std::vec::Vec { - let mut output = std::vec::Vec::new(); - for value in values { - output.push(crate::table_snapshot_from_pg(value)); - } - return output; -} - -/// Initializes PostgreSQL raw, core, decode and materialization schemas during Tauri startup. -pub(crate) async fn initialize_postgres_schema_for_startup( - state: &crate::AppState, - splash_window: &tauri::WebviewWindow, -) { - let profile = state.active_profile(); - if !profile.database.enabled { - emit_sql_startup_splash( - splash_window, - "SQL store disabled; schema initialization skipped.", - "info", - false, - ); - tracing::debug!(target: crate::TRACING_TARGET, "database is disabled; skip SQL schema initialization"); - return; - } - if profile.database.backend != "postgres" { - emit_sql_startup_splash( - splash_window, - "SQL backend is not PostgreSQL; schema initialization skipped.", - "info", - false, - ); - tracing::debug!(target: crate::TRACING_TARGET, backend = profile.database.backend.as_str(), "database backend is not postgres; skip SQL schema initialization"); - return; - } - let options_result = postgres_store_options_from_profile( - profile, - profile.database.postgres.auto_initialize_schema, - ); - let mut options = match options_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => { - let error_message = error.to_string(); - emit_sql_startup_error(splash_window, error_message.as_str()); - tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot build PostgreSQL store options"); - return; - }, - }; - let initialize_schema = options.auto_initialize_schema; - let masked_dsn = options.masked_dsn(); - options.auto_initialize_schema = false; - emit_sql_startup_splash( - splash_window, - "Connexion PostgreSQL et vérification des tables raw/core/decode...", - "info", - false, - ); - if !initialize_schema { - emit_sql_startup_splash( - splash_window, - "Auto-initialisation désactivée : vérification sans création de table.", - "info", - true, - ); - } - tracing::debug!(target: crate::TRACING_TARGET, dsn = masked_dsn.as_str(), "start PostgreSQL schema initialization"); - let store_result = ks_store::PostgresStore::connect(options).await; - let store = match store_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => { - let error_message = error.to_string(); - emit_sql_startup_error(splash_window, error_message.as_str()); - tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL connection failed during startup schema initialization"); - return; - }, - }; - let before_result = store.known_table_diagnostics().await; - let before_tables = match before_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => { - let error_message = error.to_string(); - emit_sql_startup_error(splash_window, error_message.as_str()); - tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics before schema initialization"); - return; - }, - }; - if initialize_schema { - let initialize_result = store.initialize_store_schema().await; - if let std::result::Result::Err(error) = initialize_result { - let error_message = error.to_string(); - emit_sql_startup_error(splash_window, error_message.as_str()); - tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "PostgreSQL store schema initialization failed"); - return; - } - } - let after_result = store.known_table_diagnostics().await; - let after_tables = match after_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => { - let error_message = error.to_string(); - emit_sql_startup_error(splash_window, error_message.as_str()); - tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "cannot read table diagnostics after schema initialization"); - return; - }, - }; - emit_sql_startup_table_report(splash_window, before_tables.as_slice(), after_tables.as_slice()); -} - -/// Formats debug enums for UI display. -pub(crate) fn debug_status(value: T) -> std::string::String { - return format!("{value:?}"); -} - -fn emit_sql_startup_table_report( - splash_window: &tauri::WebviewWindow, - before_tables: &[ks_store::PostgresTableDiagnostics], - after_tables: &[ks_store::PostgresTableDiagnostics], -) { - let mut created_count = 0_u32; - let mut existing_count = 0_u32; - let mut missing_count = 0_u32; - for table in after_tables { - let before_exists = table_exists_in(before_tables, table.table_name.as_str()); - if table.exists && before_exists == std::option::Option::Some(false) { - created_count += 1; - emit_sql_startup_splash( - splash_window, - format!("Created table {}", table.table_name).as_str(), - "success", - true, - ); - tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table created"); - } else if table.exists { - existing_count += 1; - emit_sql_startup_splash( - splash_window, - format!("Table {} already exists", table.table_name).as_str(), - "info", - true, - ); - tracing::debug!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table already exists"); - } else { - missing_count += 1; - emit_sql_startup_splash( - splash_window, - format!("Table {} is missing after initialization", table.table_name).as_str(), - "warning", - true, - ); - tracing::error!(target: crate::TRACING_TARGET, table = table.table_name.as_str(), "PostgreSQL table missing after initialization"); - } - } - emit_sql_startup_splash( - splash_window, - format!( - "PostgreSQL schema ready: {created_count} created, {existing_count} already present, {missing_count} missing." - ) - .as_str(), - schema_report_status(missing_count), - false, - ); - tracing::debug!(target: crate::TRACING_TARGET, created = created_count, existing = existing_count, missing = missing_count, "PostgreSQL schema initialization completed"); -} - -fn table_exists_in( - tables: &[ks_store::PostgresTableDiagnostics], - table_name: &str, -) -> std::option::Option { - for table in tables { - if table.table_name.as_str() == table_name { - return std::option::Option::Some(table.exists); - } - } - return std::option::Option::None; -} - -fn schema_report_status(missing_count: u32) -> &'static str { - if missing_count == 0 { - return "success"; - } - return "warning"; -} - -fn emit_sql_startup_error(splash_window: &tauri::WebviewWindow, message: &str) { - emit_sql_startup_splash( - splash_window, - format!("PostgreSQL schema initialization error: {message}").as_str(), - "danger", - false, - ); -} - -fn emit_sql_startup_splash( - splash_window: &tauri::WebviewWindow, - message: &str, - status: &str, - log_only: bool, -) { - let order = if log_only { "add_log" } else { "add_msg" }; - crate::emit_splash_order( - splash_window, - order, - std::option::Option::Some(message), - std::option::Option::Some(status), - std::option::Option::None, - ); -} diff --git a/kb-app-demo-desktop/src/demo_sql_diag.rs b/kb-app-demo-desktop/src/demo_sql_diag.rs deleted file mode 100644 index 0d204a5..0000000 --- a/kb-app-demo-desktop/src/demo_sql_diag.rs +++ /dev/null @@ -1,73 +0,0 @@ -// file: kb-app-demo-desktop/src/demo_sql_diag.rs -// version: 5 - -//! SQL diagnostic demo commands. - -use ts_rs::TS; // rust-rules: derive-import - -/// Complete SQL diagnostic payload shown by `demo_sql_diag`. -#[derive(Clone, Debug, serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts( - export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload.ts" -)] -pub(crate) struct DemoSqlDiagPayload { - /// Configuration file path. - pub(crate) config_path: std::string::String, - /// Active profile name. - pub(crate) active_profile_name: std::string::String, - /// Configured database backend. - pub(crate) backend: std::string::String, - /// Masked PostgreSQL DSN. - pub(crate) masked_dsn: std::string::String, - /// PostgreSQL current schema. - pub(crate) current_schema: std::option::Option, - /// PostgreSQL health status. - pub(crate) health_status: std::string::String, - /// PostgreSQL health message. - pub(crate) health_message: std::option::Option, - /// Migration status. - pub(crate) migration_status: std::string::String, - /// Migration diagnostic message. - pub(crate) migration_message: std::option::Option, - /// PostgreSQL server version when available. - pub(crate) server_version: std::option::Option, - /// Known raw/core table diagnostics. - pub(crate) tables: std::vec::Vec, -} - -/// Loads read-only SQL diagnostics from the active profile. -pub(crate) async fn load_demo_sql_diag( - state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - let profile = state.active_profile().clone(); - let store_result = crate::connect_postgres_store(&profile).await; - let store = match store_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let diagnostics_result = store.backend_diagnostics().await; - let diagnostics = match diagnostics_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - }; - let tables_result = store.known_table_diagnostics().await; - let tables = match tables_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - }; - return std::result::Result::Ok(crate::DemoSqlDiagPayload { - config_path: state.config_path().to_string(), - active_profile_name: profile.name.clone(), - backend: profile.database.backend.clone(), - masked_dsn: store.options().masked_dsn(), - current_schema: diagnostics.descriptor.current_schema, - health_status: crate::debug_status(diagnostics.health.status), - health_message: diagnostics.health.message, - migration_status: crate::debug_status(diagnostics.migrations.status), - migration_message: diagnostics.migrations.message, - server_version: diagnostics.server_version, - tables: crate::table_snapshots_from_pg(tables.as_slice()), - }); -} diff --git a/kb-app-demo-desktop/src/demo_sql_pg_core.rs b/kb-app-demo-desktop/src/demo_sql_pg_core.rs deleted file mode 100644 index 79b9395..0000000 --- a/kb-app-demo-desktop/src/demo_sql_pg_core.rs +++ /dev/null @@ -1,44 +0,0 @@ -// file: kb-app-demo-desktop/src/demo_sql_pg_core.rs -// version: 6 - -//! PostgreSQL core store demo commands. - -use ts_rs::TS; // rust-rules: derive-import - -/// PostgreSQL core store payload shown by `demo_sql_pg_core`. -#[derive(Clone, Debug, serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts( - export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload.ts" -)] -pub(crate) struct DemoSqlPgCorePayload { - /// Active profile name. - pub(crate) active_profile_name: std::string::String, - /// Masked PostgreSQL DSN. - pub(crate) masked_dsn: std::string::String, - /// Core table diagnostics. - pub(crate) tables: std::vec::Vec, -} - -/// Loads read-only core table diagnostics from PostgreSQL. -pub(crate) async fn load_demo_sql_pg_core( - state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - let profile = state.active_profile().clone(); - let store_result = crate::connect_postgres_store(&profile).await; - let store = match store_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let tables_result = store.core_table_diagnostics().await; - let tables = match tables_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - }; - return std::result::Result::Ok(crate::DemoSqlPgCorePayload { - active_profile_name: profile.name.clone(), - masked_dsn: store.options().masked_dsn(), - tables: crate::table_snapshots_from_pg(tables.as_slice()), - }); -} diff --git a/kb-app-demo-desktop/src/demo_sql_pg_raw.rs b/kb-app-demo-desktop/src/demo_sql_pg_raw.rs deleted file mode 100644 index 96e7601..0000000 --- a/kb-app-demo-desktop/src/demo_sql_pg_raw.rs +++ /dev/null @@ -1,44 +0,0 @@ -// file: kb-app-demo-desktop/src/demo_sql_pg_raw.rs -// version: 7 - -//! PostgreSQL canonical acquisition store demo commands. - -use ts_rs::TS; // rust-rules: derive-import - -/// PostgreSQL canonical acquisition payload shown by `demo_sql_pg_raw`. -#[derive(Clone, Debug, serde::Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts( - export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload.ts" -)] -pub(crate) struct DemoSqlPgRawPayload { - /// Active profile name. - pub(crate) active_profile_name: std::string::String, - /// Masked PostgreSQL DSN. - pub(crate) masked_dsn: std::string::String, - /// Raw table diagnostics. - pub(crate) tables: std::vec::Vec, -} - -/// Loads read-only raw table diagnostics from PostgreSQL. -pub(crate) async fn load_demo_sql_pg_raw( - state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - let profile = state.active_profile().clone(); - let store_result = crate::connect_postgres_store(&profile).await; - let store = match store_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let tables_result = store.raw_table_diagnostics().await; - let tables = match tables_result { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), - }; - return std::result::Result::Ok(crate::DemoSqlPgRawPayload { - active_profile_name: profile.name.clone(), - masked_dsn: store.options().masked_dsn(), - tables: crate::table_snapshots_from_pg(tables.as_slice()), - }); -} diff --git a/kb-app-demo-desktop/src/demo_store_common.rs b/kb-app-demo-desktop/src/demo_store_common.rs new file mode 100644 index 0000000..e4a85d8 --- /dev/null +++ b/kb-app-demo-desktop/src/demo_store_common.rs @@ -0,0 +1,264 @@ +// file: kb-app-demo-desktop/src/demo_store_common.rs +// version: 19 + +//! Shared backend-agnostic store demo helpers and serializable payloads. + +use ts_rs::TS; // rust-rules: derive-import + +/// UI-safe diagnostics for one logical store resource. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store/DemoStoreResourceSnapshot.ts" +)] +pub(crate) struct DemoStoreResourceSnapshot { + /// Stable logical resource code independent of the active backend. + pub(crate) resource_code: std::string::String, + /// Logical store model containing the resource. + pub(crate) model_code: std::string::String, + /// Human-readable resource role. + pub(crate) role: std::string::String, + /// Whether the logical resource is currently available. + pub(crate) available: bool, + /// Record count when the resource is available. + #[ts(type = "number | null")] + pub(crate) record_count: std::option::Option, + /// Minimum slot when available. + #[ts(type = "number | null")] + pub(crate) min_slot: std::option::Option, + /// Maximum slot when available. + #[ts(type = "number | null")] + pub(crate) max_slot: std::option::Option, + /// Latest persistence timestamp when available. + pub(crate) latest_created_at: std::option::Option, +} + +/// Builds backend-agnostic store-open options from one resolved profile. +fn store_open_options_from_profile( + profile: &ks_config::ProfileConfig, +) -> ks_core::Result { + return ks_store::StoreOpenOptions::new( + profile.database.enabled, + profile.database.backend.clone(), + profile.database.backend_options.clone(), + ); +} + +/// Opens a backend-agnostic store from one resolved profile. +/// +/// Prefer [`crate::AppState::store`] for desktop commands that already own application state. +pub(crate) async fn open_store( + profile: &ks_config::ProfileConfig, +) -> std::result::Result { + let options = match store_open_options_from_profile(profile) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + return match ks_store::Store::open(options).await { + std::result::Result::Ok(store) => std::result::Result::Ok(store), + std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), + }; +} + +/// Clones the persistent store held by application state. +pub(crate) async fn shared_store( + state: &crate::AppState, +) -> std::result::Result { + let store_result = state.store().await; + return match store_result { + std::result::Result::Ok(store) => std::result::Result::Ok(store.clone()), + std::result::Result::Err(error) => std::result::Result::Err(error.to_string()), + }; +} + +/// Converts one logical store resource diagnostic to the UI payload shape. +pub(crate) fn store_resource_snapshot( + value: &ks_store::StoreResourceDiagnostics, +) -> crate::DemoStoreResourceSnapshot { + let record_count = match &value.statistics { + std::option::Option::Some(statistics) => std::option::Option::Some(statistics.record_count), + std::option::Option::None => std::option::Option::None, + }; + let min_slot = match &value.statistics { + std::option::Option::Some(statistics) => statistics.min_slot, + std::option::Option::None => std::option::Option::None, + }; + let max_slot = match &value.statistics { + std::option::Option::Some(statistics) => statistics.max_slot, + std::option::Option::None => std::option::Option::None, + }; + let latest_created_at = match &value.statistics { + std::option::Option::Some(statistics) => statistics.latest_created_at.clone(), + std::option::Option::None => std::option::Option::None, + }; + return crate::DemoStoreResourceSnapshot { + resource_code: value.resource_code.clone(), + model_code: value.model_code.clone(), + role: value.role.clone(), + available: value.available, + record_count, + min_slot, + max_slot, + latest_created_at, + }; +} + +/// Converts many logical store resource diagnostics to UI payloads. +pub(crate) fn store_resource_snapshots( + values: &[ks_store::StoreResourceDiagnostics], +) -> std::vec::Vec { + let mut output = std::vec::Vec::with_capacity(values.len()); + for value in values { + output.push(crate::store_resource_snapshot(value)); + } + return output; +} + +/// Opens, auto-initializes when configured, and verifies the persistent store during startup. +pub(crate) async fn initialize_store_for_startup( + state: &crate::AppState, + splash_window: &tauri::WebviewWindow, +) { + let store_result = state.store().await; + let store = match store_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + let error_message = error.to_string(); + emit_store_startup_error(splash_window, error_message.as_str()); + tracing::error!(target: crate::TRACING_TARGET, error = error_message.as_str(), "store startup failed"); + return; + }, + }; + let configuration = store.configuration_summary(); + if !configuration.enabled { + emit_store_startup_splash( + splash_window, + "Store disabled; initialization skipped.", + "info", + false, + ); + tracing::debug!(target: crate::TRACING_TARGET, backend = configuration.backend_code.as_str(), "store disabled; startup initialization skipped"); + return; + } + emit_store_startup_splash( + splash_window, + format!( + "Store backend {}: vérification des modèles de persistance...", + configuration.backend_code + ) + .as_str(), + "info", + false, + ); + let initialization = store.initialization_summary(); + for model in &initialization.models { + emit_store_startup_splash( + splash_window, + format!( + "Modèle {}: {}/{} ressources disponibles, {} créées.", + model.model_code, + model.available_resource_count, + model.expected_resource_count, + model.created_resource_count + ) + .as_str(), + model_status(&model.status), + true, + ); + } + for object in &initialization.objects { + emit_store_startup_splash( + splash_window, + format!( + "Objets {}: {}/{} disponibles, {} créés.", + object.object_kind, + object.available_count, + object.expected_count, + object.created_count + ) + .as_str(), + "info", + true, + ); + } + emit_store_startup_splash( + splash_window, + format!( + "Store {:?}: ressources {}/{}, {} créées.", + initialization.status, + initialization.available_resource_count, + initialization.expected_resource_count, + initialization.created_resource_count + ) + .as_str(), + initialization_status(&initialization.status), + false, + ); + tracing::debug!(target: crate::TRACING_TARGET, backend = configuration.backend_code.as_str(), available_resources = initialization.available_resource_count, expected_resources = initialization.expected_resource_count, created_resources = initialization.created_resource_count, "store startup verification completed"); +} + +/// Returns the safe connection descriptor exposed by the active backend. +pub(crate) async fn store_connection_descriptor( + store: &ks_store::Store, +) -> std::result::Result { + let runtime = match store.runtime_summary().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + let backend = match runtime.backend { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::result::Result::Ok("disabled".to_string()), + }; + let descriptor = match backend.descriptor.masked_connection_descriptor { + std::option::Option::Some(value) => value, + std::option::Option::None => "configured".to_string(), + }; + return std::result::Result::Ok(descriptor); +} + +/// Formats debug enums for UI display. +pub(crate) fn debug_status(value: T) -> std::string::String { + return format!("{value:?}"); +} + +fn model_status(status: &ks_store::StoreModelVerificationStatus) -> &'static str { + return match status { + ks_store::StoreModelVerificationStatus::Ready => "success", + ks_store::StoreModelVerificationStatus::Incomplete => "warning", + }; +} + +fn initialization_status(status: &ks_store::StoreInitializationStatus) -> &'static str { + return match status { + ks_store::StoreInitializationStatus::Ready => "success", + ks_store::StoreInitializationStatus::Disabled => "info", + ks_store::StoreInitializationStatus::Partial => "warning", + ks_store::StoreInitializationStatus::Failed => "danger", + }; +} + +fn emit_store_startup_error(splash_window: &tauri::WebviewWindow, message: &str) { + emit_store_startup_splash( + splash_window, + format!("Store initialization error: {message}").as_str(), + "danger", + false, + ); +} + +fn emit_store_startup_splash( + splash_window: &tauri::WebviewWindow, + message: &str, + status: &str, + log_only: bool, +) { + let order = if log_only { "add_log" } else { "add_msg" }; + crate::emit_splash_order( + splash_window, + order, + std::option::Option::Some(message), + std::option::Option::Some(status), + std::option::Option::None, + ); +} diff --git a/kb-app-demo-desktop/src/demo_store_core.rs b/kb-app-demo-desktop/src/demo_store_core.rs new file mode 100644 index 0000000..d428203 --- /dev/null +++ b/kb-app-demo-desktop/src/demo_store_core.rs @@ -0,0 +1,45 @@ +// file: kb-app-demo-desktop/src/demo_store_core.rs +// version: 9 + +//! Backend-agnostic Core store demo commands retained by the store window. + +use ts_rs::TS; // rust-rules: derive-import + +/// Backend-agnostic Core store payload shown by the existing `demo_store_core` window. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_core/DemoStoreCorePayload.ts" +)] +pub(crate) struct DemoStoreCorePayload { + /// Active profile name. + pub(crate) active_profile_name: std::string::String, + /// Safe backend connection descriptor. + pub(crate) connection_descriptor: std::string::String, + /// Logical Core-resource diagnostics. + pub(crate) resources: std::vec::Vec, +} + +/// Loads read-only Core resource diagnostics from the active store. +pub(crate) async fn load_demo_store_core( + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let store = match crate::shared_store(state.inner()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let resources = match store.core_resource_diagnostics().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + let connection_descriptor = match crate::store_connection_descriptor(&store).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::DemoStoreCorePayload { + active_profile_name: state.active_profile().name.clone(), + connection_descriptor, + resources: crate::store_resource_snapshots(resources.as_slice()), + }); +} diff --git a/kb-app-demo-desktop/src/demo_store_diag.rs b/kb-app-demo-desktop/src/demo_store_diag.rs new file mode 100644 index 0000000..6526342 --- /dev/null +++ b/kb-app-demo-desktop/src/demo_store_diag.rs @@ -0,0 +1,81 @@ +// file: kb-app-demo-desktop/src/demo_store_diag.rs +// version: 9 + +//! Backend-agnostic store diagnostic demo commands retained by the Store diagnostics window. + +use ts_rs::TS; // rust-rules: derive-import + +/// Complete backend-neutral store diagnostic payload shown by `demo_store_diag`. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_diag/DemoStoreDiagPayload.ts" +)] +pub(crate) struct DemoStoreDiagPayload { + /// Configuration file path. + pub(crate) config_path: std::string::String, + /// Active profile name. + pub(crate) active_profile_name: std::string::String, + /// Active backend code. + pub(crate) backend: std::string::String, + /// Safe backend connection descriptor. + pub(crate) connection_descriptor: std::string::String, + /// Backend namespace, schema, or equivalent logical location. + pub(crate) namespace: std::option::Option, + /// Store health status. + pub(crate) health_status: std::string::String, + /// Store health message. + pub(crate) health_message: std::option::Option, + /// Migration status. + pub(crate) migration_status: std::string::String, + /// Migration diagnostic message. + pub(crate) migration_message: std::option::Option, + /// Backend version when available. + pub(crate) backend_version: std::option::Option, + /// Known logical store resource diagnostics. + pub(crate) resources: std::vec::Vec, +} + +/// Loads read-only diagnostics from the active store. +pub(crate) async fn load_demo_store_diag( + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let store = match crate::shared_store(state.inner()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let runtime = match store.runtime_summary().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + let resources = match store.known_resource_diagnostics().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + let backend = match runtime.backend { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + "store backend diagnostics are unavailable".to_string(), + ); + }, + }; + let connection_descriptor = match backend.descriptor.masked_connection_descriptor.clone() { + std::option::Option::Some(value) => value, + std::option::Option::None => "configured".to_string(), + }; + return std::result::Result::Ok(crate::DemoStoreDiagPayload { + config_path: state.config_path().to_string(), + active_profile_name: state.active_profile().name.clone(), + backend: backend.descriptor.backend_code, + connection_descriptor, + namespace: backend.descriptor.namespace, + health_status: crate::debug_status(backend.health.status), + health_message: backend.health.message, + migration_status: crate::debug_status(backend.migrations.status), + migration_message: backend.migrations.message, + backend_version: backend.backend_version, + resources: crate::store_resource_snapshots(resources.as_slice()), + }); +} diff --git a/kb-app-demo-desktop/src/demo_store_raw.rs b/kb-app-demo-desktop/src/demo_store_raw.rs new file mode 100644 index 0000000..929afa1 --- /dev/null +++ b/kb-app-demo-desktop/src/demo_store_raw.rs @@ -0,0 +1,45 @@ +// file: kb-app-demo-desktop/src/demo_store_raw.rs +// version: 10 + +//! Backend-agnostic level-1 raw store demo commands retained by the store window. + +use ts_rs::TS; // rust-rules: derive-import + +/// Backend-agnostic raw store payload shown by the existing `demo_store_raw` window. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts( + export, + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_raw/DemoStoreRawPayload.ts" +)] +pub(crate) struct DemoStoreRawPayload { + /// Active profile name. + pub(crate) active_profile_name: std::string::String, + /// Safe backend connection descriptor. + pub(crate) connection_descriptor: std::string::String, + /// Logical raw-resource diagnostics. + pub(crate) resources: std::vec::Vec, +} + +/// Loads read-only level-1 raw resource diagnostics from the active store. +pub(crate) async fn load_demo_store_raw( + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let store = match crate::shared_store(state.inner()).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let resources = match store.raw_resource_diagnostics().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + }; + let connection_descriptor = match crate::store_connection_descriptor(&store).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::DemoStoreRawPayload { + active_profile_name: state.active_profile().name.clone(), + connection_descriptor, + resources: crate::store_resource_snapshots(resources.as_slice()), + }); +} diff --git a/kb-app-demo-desktop/src/demo_sql_replay_candidates.rs b/kb-app-demo-desktop/src/demo_store_replay_candidates.rs similarity index 78% rename from kb-app-demo-desktop/src/demo_sql_replay_candidates.rs rename to kb-app-demo-desktop/src/demo_store_replay_candidates.rs index c5b63e8..f79750c 100644 --- a/kb-app-demo-desktop/src/demo_sql_replay_candidates.rs +++ b/kb-app-demo-desktop/src/demo_store_replay_candidates.rs @@ -1,7 +1,7 @@ -// file: kb-app-demo-desktop/src/demo_sql_replay_candidates.rs -// version: 13 +// file: kb-app-demo-desktop/src/demo_store_replay_candidates.rs +// version: 16 -//! Read-only SQL replay candidate browser commands. +//! Backend-agnostic read-only replay candidate browser commands. use ts_rs::TS; // rust-rules: derive-import @@ -10,9 +10,9 @@ use ts_rs::TS; // rust-rules: derive-import #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayKnownProgramOption.ts" )] -pub(crate) struct DemoSqlReplayKnownProgramOption { +pub(crate) struct DemoStoreReplayKnownProgramOption { /// Stable lower snake case program code. pub(crate) code: std::string::String, /// Base58 Solana program identifier. @@ -24,17 +24,17 @@ pub(crate) struct DemoSqlReplayKnownProgramOption { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayOptionsPayload.ts" )] -pub(crate) struct DemoSqlReplayOptionsPayload { +pub(crate) struct DemoStoreReplayOptionsPayload { /// Active profile name. pub(crate) active_profile_name: std::string::String, - /// Masked PostgreSQL DSN. - pub(crate) masked_dsn: std::string::String, + /// Safe backend connection descriptor. + pub(crate) connection_descriptor: std::string::String, /// Maximum rows accepted by one query. pub(crate) maximum_limit: u32, /// Program identifiers enumerable from `ks_program_ids`. - pub(crate) known_programs: std::vec::Vec, + pub(crate) known_programs: std::vec::Vec, } /// UI request for bounded transaction replay candidates. @@ -42,9 +42,9 @@ pub(crate) struct DemoSqlReplayOptionsPayload { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRequest.ts" )] -pub(crate) struct DemoSqlReplayTransactionRequest { +pub(crate) struct DemoStoreReplayTransactionRequest { /// Optional partial signature search. pub(crate) signature_contains: std::option::Option, /// Optional inclusive minimum slot. @@ -59,7 +59,7 @@ pub(crate) struct DemoSqlReplayTransactionRequest { pub(crate) ledger_status: std::option::Option, /// Optional exact program id. pub(crate) program_id: std::option::Option, - /// Program scope code: any, outer, inner or logs. + /// Program scope code: any, top_level, inner or logs. pub(crate) program_scope: std::string::String, /// Optional entity kind: mint, owner or account_key. pub(crate) entity_kind: std::option::Option, @@ -76,9 +76,9 @@ pub(crate) struct DemoSqlReplayTransactionRequest { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRow.ts" )] -pub(crate) struct DemoSqlReplayTransactionRow { +pub(crate) struct DemoStoreReplayTransactionRow { /// Canonical transaction signature. pub(crate) signature: std::string::String, /// Transaction slot. @@ -100,13 +100,13 @@ pub(crate) struct DemoSqlReplayTransactionRow { pub(crate) attempt_count: i32, /// Number of top-level instructions. #[ts(type = "number")] - pub(crate) outer_instruction_count: i64, + pub(crate) top_level_instruction_count: i64, /// Number of inner instructions. #[ts(type = "number")] pub(crate) inner_instruction_count: i64, /// Number of distinct top-level programs. #[ts(type = "number")] - pub(crate) outer_program_count: i64, + pub(crate) top_level_program_count: i64, /// Number of distinct inner programs. #[ts(type = "number")] pub(crate) inner_program_count: i64, @@ -119,9 +119,9 @@ pub(crate) struct DemoSqlReplayTransactionRow { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRequest.ts" )] -pub(crate) struct DemoSqlReplayProgramRequest { +pub(crate) struct DemoStoreReplayProgramRequest { /// Optional partial program id search. pub(crate) program_id_contains: std::option::Option, /// Maximum returned rows. @@ -133,9 +133,9 @@ pub(crate) struct DemoSqlReplayProgramRequest { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRow.ts" )] -pub(crate) struct DemoSqlReplayProgramRow { +pub(crate) struct DemoStoreReplayProgramRow { /// Optional stable code from `ks_program_ids`. pub(crate) program_code: std::option::Option, /// Program id. @@ -145,7 +145,7 @@ pub(crate) struct DemoSqlReplayProgramRow { pub(crate) transaction_count: i64, /// Number of top-level instruction occurrences. #[ts(type = "number")] - pub(crate) outer_instruction_count: i64, + pub(crate) top_level_instruction_count: i64, /// Number of inner instruction occurrences. #[ts(type = "number")] pub(crate) inner_instruction_count: i64, @@ -165,9 +165,9 @@ pub(crate) struct DemoSqlReplayProgramRow { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRequest.ts" )] -pub(crate) struct DemoSqlReplayEntityRequest { +pub(crate) struct DemoStoreReplayEntityRequest { /// Entity kind: mint, owner or account_key. pub(crate) entity_kind: std::string::String, /// Optional partial entity value search. @@ -181,9 +181,9 @@ pub(crate) struct DemoSqlReplayEntityRequest { #[serde(rename_all = "camelCase")] #[ts( export, - export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow.ts" + export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRow.ts" )] -pub(crate) struct DemoSqlReplayEntityRow { +pub(crate) struct DemoStoreReplayEntityRow { /// Stable entity kind code. pub(crate) entity_kind: std::string::String, /// Mint, owner or account-key address. @@ -203,7 +203,7 @@ pub(crate) struct DemoSqlReplayEntityRow { } /// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`. -pub(crate) async fn export_demo_sql_replay_csv( +pub(crate) async fn export_demo_store_replay_csv( file_name: std::string::String, content: std::string::String, ) -> std::result::Result { @@ -240,37 +240,40 @@ pub(crate) async fn export_demo_sql_replay_csv( return std::result::Result::Ok(display_path); } -/// Loads static browser options from the active PostgreSQL profile. -pub(crate) async fn demo_sql_replay_options( +/// Loads static browser options from the active store. +pub(crate) async fn demo_store_replay_options( state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { +) -> std::result::Result { let profile = state.active_profile().clone(); - let options_result = crate::postgres_store_options_from_profile(&profile, false); - let options = match options_result { + let store = match crate::shared_store(state.inner()).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let connection_descriptor = match crate::store_connection_descriptor(&store).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut known_programs = std::vec::Vec::with_capacity(ks_program_ids::registered_program_ids().len()); for entry in ks_program_ids::registered_program_ids() { - known_programs.push(crate::DemoSqlReplayKnownProgramOption { + known_programs.push(crate::DemoStoreReplayKnownProgramOption { code: entry.code().to_owned(), program_id: entry.program_id().to_owned(), }); } - return std::result::Result::Ok(crate::DemoSqlReplayOptionsPayload { + return std::result::Result::Ok(crate::DemoStoreReplayOptionsPayload { active_profile_name: profile.name, - masked_dsn: options.masked_dsn(), + connection_descriptor, maximum_limit: ks_store::MAX_REPLAY_CANDIDATE_ROWS, known_programs, }); } -/// Loads bounded transaction replay candidates from PostgreSQL. -pub(crate) async fn load_demo_sql_replay_transactions( +/// Loads bounded transaction replay candidates from the active store. +pub(crate) async fn load_demo_store_replay_transactions( state: tauri::State<'_, crate::AppState>, - request: crate::DemoSqlReplayTransactionRequest, -) -> std::result::Result, std::string::String> { + request: crate::DemoStoreReplayTransactionRequest, +) -> std::result::Result, std::string::String> { let program_scope_result = program_scope_from_code(request.program_scope.as_str()); let program_scope = match program_scope_result { std::result::Result::Ok(value) => value, @@ -281,7 +284,7 @@ pub(crate) async fn load_demo_sql_replay_transactions( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let filter_result = ks_store::PostgresReplayTransactionFilter::new( + let filter_result = ks_store::ReplayTransactionFilter::new( request.signature_contains, request.min_slot, request.max_slot, @@ -298,7 +301,7 @@ pub(crate) async fn load_demo_sql_replay_transactions( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state.inner()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -311,23 +314,23 @@ pub(crate) async fn load_demo_sql_replay_transactions( tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay transaction candidates"); let mut output = std::vec::Vec::with_capacity(rows.len()); for row in rows { - output.push(transaction_row_from_pg(row)); + output.push(transaction_row_from_store(row)); } return std::result::Result::Ok(output); } -/// Loads bounded program summaries from PostgreSQL. -pub(crate) async fn load_demo_sql_replay_programs( +/// Loads bounded program summaries from the active store. +pub(crate) async fn load_demo_store_replay_programs( state: tauri::State<'_, crate::AppState>, - request: crate::DemoSqlReplayProgramRequest, -) -> std::result::Result, std::string::String> { + request: crate::DemoStoreReplayProgramRequest, +) -> std::result::Result, std::string::String> { let filter_result = - ks_store::PostgresReplayProgramFilter::new(request.program_id_contains, request.limit); + ks_store::ReplayProgramFilter::new(request.program_id_contains, request.limit); let filter = match filter_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state.inner()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -340,22 +343,22 @@ pub(crate) async fn load_demo_sql_replay_programs( tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay program summaries"); let mut output = std::vec::Vec::with_capacity(rows.len()); for row in rows { - output.push(program_row_from_pg(row)); + output.push(program_row_from_store(row)); } return std::result::Result::Ok(output); } -/// Loads bounded mint, owner or account-key summaries from PostgreSQL core tables. -pub(crate) async fn load_demo_sql_replay_entities( +/// Loads bounded mint, owner or account-key summaries from Core facts. +pub(crate) async fn load_demo_store_replay_entities( state: tauri::State<'_, crate::AppState>, - request: crate::DemoSqlReplayEntityRequest, -) -> std::result::Result, std::string::String> { + request: crate::DemoStoreReplayEntityRequest, +) -> std::result::Result, std::string::String> { let entity_kind_result = entity_kind_from_code(request.entity_kind.as_str()); let entity_kind = match entity_kind_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let filter_result = ks_store::PostgresReplayEntityFilter::new( + let filter_result = ks_store::ReplayEntityFilter::new( entity_kind, request.entity_value_contains, request.limit, @@ -364,7 +367,7 @@ pub(crate) async fn load_demo_sql_replay_entities( std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; - let store_result = crate::connect_postgres_store(state.active_profile()).await; + let store_result = crate::shared_store(state.inner()).await; let store = match store_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -377,15 +380,15 @@ pub(crate) async fn load_demo_sql_replay_entities( tracing::debug!(target: crate::TRACING_TARGET, rows = rows.len(), "loaded replay entity summaries"); let mut output = std::vec::Vec::with_capacity(rows.len()); for row in rows { - output.push(entity_row_from_pg(row)); + output.push(entity_row_from_store(row)); } return std::result::Result::Ok(output); } -fn transaction_row_from_pg( - row: ks_store::PostgresReplayTransactionCandidate, -) -> crate::DemoSqlReplayTransactionRow { - return crate::DemoSqlReplayTransactionRow { +fn transaction_row_from_store( + row: ks_store::ReplayTransactionCandidate, +) -> crate::DemoStoreReplayTransactionRow { + return crate::DemoStoreReplayTransactionRow { signature: row.signature, slot: row.slot, raw_processing_state: row.raw_processing_state, @@ -395,24 +398,22 @@ fn transaction_row_from_pg( ledger_status: row.ledger_status, processor_version: row.processor_version, attempt_count: row.attempt_count, - outer_instruction_count: row.outer_instruction_count, + top_level_instruction_count: row.top_level_instruction_count, inner_instruction_count: row.inner_instruction_count, - outer_program_count: row.outer_program_count, + top_level_program_count: row.top_level_program_count, inner_program_count: row.inner_program_count, updated_at: row.updated_at, }; } -fn program_row_from_pg( - row: ks_store::PostgresReplayProgramSummary, -) -> crate::DemoSqlReplayProgramRow { +fn program_row_from_store(row: ks_store::ReplayProgramSummary) -> crate::DemoStoreReplayProgramRow { let program_code = ks_program_ids::find_registered_program_id(&row.program_id) .map(|entry| return entry.code().to_owned()); - return crate::DemoSqlReplayProgramRow { + return crate::DemoStoreReplayProgramRow { program_code, program_id: row.program_id, transaction_count: row.transaction_count, - outer_instruction_count: row.outer_instruction_count, + top_level_instruction_count: row.top_level_instruction_count, inner_instruction_count: row.inner_instruction_count, log_count: row.log_count, min_slot: row.min_slot, @@ -420,8 +421,8 @@ fn program_row_from_pg( }; } -fn entity_row_from_pg(row: ks_store::PostgresReplayEntitySummary) -> crate::DemoSqlReplayEntityRow { - return crate::DemoSqlReplayEntityRow { +fn entity_row_from_store(row: ks_store::ReplayEntitySummary) -> crate::DemoStoreReplayEntityRow { + return crate::DemoStoreReplayEntityRow { entity_kind: row.entity_kind, entity_value: row.entity_value, transaction_count: row.transaction_count, @@ -480,20 +481,19 @@ async fn available_csv_export_path( fn program_scope_from_code( code: &str, -) -> std::result::Result { +) -> std::result::Result { return match code { - "any" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Any), - "outer" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Outer), - "inner" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Inner), - "logs" => std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Logs), + "any" => std::result::Result::Ok(ks_store::ReplayProgramScope::Any), + "top_level" => std::result::Result::Ok(ks_store::ReplayProgramScope::TopLevel), + "inner" => std::result::Result::Ok(ks_store::ReplayProgramScope::Inner), + "logs" => std::result::Result::Ok(ks_store::ReplayProgramScope::Logs), _ => std::result::Result::Err(format!("unsupported replay program scope: {code}")), }; } fn optional_entity_kind_from_code( code: std::option::Option<&str>, -) -> std::result::Result, std::string::String> -{ +) -> std::result::Result, std::string::String> { return match code { std::option::Option::Some(value) => { let result = entity_kind_from_code(value); @@ -510,11 +510,11 @@ fn optional_entity_kind_from_code( fn entity_kind_from_code( code: &str, -) -> std::result::Result { +) -> std::result::Result { return match code { - "mint" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::Mint), - "owner" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::Owner), - "account_key" => std::result::Result::Ok(ks_store::PostgresReplayEntityKind::AccountKey), + "mint" => std::result::Result::Ok(ks_store::ReplayEntityKind::Mint), + "owner" => std::result::Result::Ok(ks_store::ReplayEntityKind::Owner), + "account_key" => std::result::Result::Ok(ks_store::ReplayEntityKind::AccountKey), _ => std::result::Result::Err(format!("unsupported replay entity kind: {code}")), }; } @@ -524,7 +524,7 @@ mod tests { #[test] fn program_scope_parser_accepts_logs() { let result = super::program_scope_from_code("logs"); - assert_eq!(result, std::result::Result::Ok(ks_store::PostgresReplayProgramScope::Logs)); + assert_eq!(result, std::result::Result::Ok(ks_store::ReplayProgramScope::Logs)); } #[test] @@ -551,16 +551,16 @@ mod tests { #[test] fn program_row_exposes_registered_code() { - let row = ks_store::PostgresReplayProgramSummary { + let row = ks_store::ReplayProgramSummary { program_id: ks_program_ids::SYSTEM_PROGRAM_ID.to_owned(), transaction_count: 1, - outer_instruction_count: 1, + top_level_instruction_count: 1, inner_instruction_count: 0, log_count: 1, min_slot: 1, max_slot: 1, }; - let converted = super::program_row_from_pg(row); + let converted = super::program_row_from_store(row); assert_eq!(converted.program_code.as_deref(), std::option::Option::Some("system")); } } diff --git a/kb-app-demo-desktop/src/demo_ws.rs b/kb-app-demo-desktop/src/demo_ws.rs index 3119a6b..510c84e 100644 --- a/kb-app-demo-desktop/src/demo_ws.rs +++ b/kb-app-demo-desktop/src/demo_ws.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/demo_ws.rs -// version: 23 +// version: 24 //! Standard Solana WebSocket demo commands backed by `ks_onchain_transport::WsSession`. @@ -1031,7 +1031,7 @@ mod tests { ::decl(&config), ::decl(&config), ::decl(&config), - ::decl(&config), + ::decl(&config), ]; for declaration in declarations { assert!(!declaration.contains("bigint")); diff --git a/kb-app-demo-desktop/src/lib.rs b/kb-app-demo-desktop/src/lib.rs index 456d9a9..3da748f 100644 --- a/kb-app-demo-desktop/src/lib.rs +++ b/kb-app-demo-desktop/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/lib.rs -// version: 43 +// version: 47 //! Tauri desktop demo application for `khadhroony-bot3`. @@ -24,11 +24,11 @@ mod demo_http; mod demo_spl_ata; mod demo_spl_token; mod demo_spl_token_2022; -mod demo_sql_common; -mod demo_sql_diag; -mod demo_sql_pg_core; -mod demo_sql_pg_raw; -mod demo_sql_replay_candidates; +mod demo_store_common; +mod demo_store_core; +mod demo_store_diag; +mod demo_store_raw; +mod demo_store_replay_candidates; mod demo_transport; mod demo_wallet; mod demo_ws; @@ -147,7 +147,7 @@ pub(crate) use self::demo_decode_replay::demo_decode_replay_diagnostics; pub(crate) use self::demo_decode_replay::demo_decode_replay_execute; /// Returns available decoders and default replay bounds. pub(crate) use self::demo_decode_replay::demo_decode_replay_options; -/// Readiness report for one Devnet profile PostgreSQL store. +/// Backend-agnostic readiness report for one Devnet profile store. pub(crate) use self::demo_devnet_common::DemoExecutionDevnetStoreReadinessPayload; /// Restores the shared single-execution state flag when a Devnet run finishes. pub(crate) use self::demo_devnet_common::DemoExecutionRunGuard; @@ -315,58 +315,60 @@ pub(crate) use self::demo_spl_token_2022::DemoSplToken2022FixturePayload; pub(crate) use self::demo_spl_token_2022::demo_execution_spl_token_2022_execute; /// Loads public Token-2022 fixture values without reading private key bytes. pub(crate) use self::demo_spl_token_2022::demo_spl_token_2022_fixture; -/// UI-safe SQL table diagnostic snapshot. -pub(crate) use self::demo_sql_common::DemoSqlTableSnapshot; -/// Connects to the configured PostgreSQL store. -pub(crate) use self::demo_sql_common::connect_postgres_store; +/// UI-safe logical store resource diagnostic snapshot. +pub(crate) use self::demo_store_common::DemoStoreResourceSnapshot; /// Formats diagnostic statuses. -pub(crate) use self::demo_sql_common::debug_status; -/// Initializes the PostgreSQL schema during startup. -pub(crate) use self::demo_sql_common::initialize_postgres_schema_for_startup; -/// Builds PostgreSQL options from the active profile. -pub(crate) use self::demo_sql_common::postgres_store_options_from_profile; -/// Converts one PostgreSQL table diagnostic. -pub(crate) use self::demo_sql_common::table_snapshot_from_pg; -/// Converts PostgreSQL table diagnostics. -pub(crate) use self::demo_sql_common::table_snapshots_from_pg; -/// SQL diagnostic payload. -pub(crate) use self::demo_sql_diag::DemoSqlDiagPayload; -/// Loads read-only SQL diagnostics from the active profile. -pub(crate) use self::demo_sql_diag::load_demo_sql_diag; -/// PostgreSQL core-table payload. -pub(crate) use self::demo_sql_pg_core::DemoSqlPgCorePayload; -/// Loads read-only core table diagnostics from PostgreSQL. -pub(crate) use self::demo_sql_pg_core::load_demo_sql_pg_core; -/// PostgreSQL raw-table payload. -pub(crate) use self::demo_sql_pg_raw::DemoSqlPgRawPayload; -/// Loads read-only raw table diagnostics from PostgreSQL. -pub(crate) use self::demo_sql_pg_raw::load_demo_sql_pg_raw; -/// SQL replay entity request. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRequest; -/// SQL replay entity row. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayEntityRow; +pub(crate) use self::demo_store_common::debug_status; +/// Opens and verifies the configured store during startup. +pub(crate) use self::demo_store_common::initialize_store_for_startup; +/// Opens the configured backend-agnostic store. +pub(crate) use self::demo_store_common::open_store; +/// Clones the persistent application store. +pub(crate) use self::demo_store_common::shared_store; +/// Returns the safe connection descriptor exposed by the active store backend. +pub(crate) use self::demo_store_common::store_connection_descriptor; +/// Converts one logical store resource diagnostic. +pub(crate) use self::demo_store_common::store_resource_snapshot; +/// Converts logical store resource diagnostics. +pub(crate) use self::demo_store_common::store_resource_snapshots; +/// Backend-agnostic Core store diagnostic payload. +pub(crate) use self::demo_store_core::DemoStoreCorePayload; +/// Loads read-only Core resource diagnostics from the active store. +pub(crate) use self::demo_store_core::load_demo_store_core; +/// Backend-agnostic store diagnostic payload. +pub(crate) use self::demo_store_diag::DemoStoreDiagPayload; +/// Loads read-only Store diagnostics from the active profile. +pub(crate) use self::demo_store_diag::load_demo_store_diag; +/// Backend-agnostic raw store diagnostic payload. +pub(crate) use self::demo_store_raw::DemoStoreRawPayload; +/// Loads read-only raw resource diagnostics from the active store. +pub(crate) use self::demo_store_raw::load_demo_store_raw; +/// Store replay entity request. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayEntityRequest; +/// Store replay entity row. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayEntityRow; /// Known program option. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayKnownProgramOption; -/// SQL replay options payload. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayOptionsPayload; -/// SQL replay program request. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRequest; -/// SQL replay program row. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayProgramRow; -/// SQL replay transaction request. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRequest; -/// SQL replay transaction row. -pub(crate) use self::demo_sql_replay_candidates::DemoSqlReplayTransactionRow; -/// Loads static browser options from the active PostgreSQL profile. -pub(crate) use self::demo_sql_replay_candidates::demo_sql_replay_options; +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayKnownProgramOption; +/// Store replay options payload. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayOptionsPayload; +/// Store replay program request. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramRequest; +/// Store replay program row. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayProgramRow; +/// Store replay transaction request. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayTransactionRequest; +/// Store replay transaction row. +pub(crate) use self::demo_store_replay_candidates::DemoStoreReplayTransactionRow; +/// Loads static replay browser options from the active store profile. +pub(crate) use self::demo_store_replay_candidates::demo_store_replay_options; /// Writes a bounded replay-candidate CSV file into `./data/exports_csv/`. -pub(crate) use self::demo_sql_replay_candidates::export_demo_sql_replay_csv; -/// Loads bounded mint, owner or account-key summaries from PostgreSQL core tables. -pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_entities; -/// Loads bounded program summaries from PostgreSQL. -pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_programs; -/// Loads bounded transaction replay candidates from PostgreSQL. -pub(crate) use self::demo_sql_replay_candidates::load_demo_sql_replay_transactions; +pub(crate) use self::demo_store_replay_candidates::export_demo_store_replay_csv; +/// Loads bounded mint, owner or account-key summaries from Core store resources. +pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_entities; +/// Loads bounded program summaries from the active store. +pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_programs; +/// Loads bounded transaction replay candidates from the active store. +pub(crate) use self::demo_store_replay_candidates::load_demo_store_replay_transactions; /// UI-safe endpoint role shared by HTTP and WebSocket diagnostics. pub(crate) use self::demo_transport::DemoEndpointRolePayload; /// Exact public SOL balance projected by the wallet demo. diff --git a/kb-app-demo-desktop/src/splash.rs b/kb-app-demo-desktop/src/splash.rs index ad3d43a..a9eae29 100644 --- a/kb-app-demo-desktop/src/splash.rs +++ b/kb-app-demo-desktop/src/splash.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/splash.rs -// version: 5 +// version: 6 //! Splash-window payloads and startup sequencing helpers. @@ -112,7 +112,7 @@ pub(crate) async fn splash_frontend_ready( std::option::Option::Some("success"), std::option::Option::None, ); - crate::initialize_postgres_schema_for_startup(state, &window).await; + crate::initialize_store_for_startup(state, &window).await; crate::emit_splash_order( &window, "add_msg", diff --git a/kb-app-demo-desktop/src/tauri.rs b/kb-app-demo-desktop/src/tauri.rs index b9c44e2..e30c02d 100644 --- a/kb-app-demo-desktop/src/tauri.rs +++ b/kb-app-demo-desktop/src/tauri.rs @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/src/tauri.rs -// version: 44 +// version: 46 //! Tauri runtime assembly and private command wrappers. @@ -71,18 +71,18 @@ pub fn run() -> ks_core::Result<()> { demo_decode_replay_cancel, demo_decode_replay_diagnostics, demo_decode_replay_annotations, - open_demo_sql_diag_window, - load_demo_sql_diag, - open_demo_sql_pg_raw_window, - load_demo_sql_pg_raw, - open_demo_sql_pg_core_window, - load_demo_sql_pg_core, - open_demo_sql_replay_candidates_window, - export_demo_sql_replay_csv, - demo_sql_replay_options, - load_demo_sql_replay_transactions, - load_demo_sql_replay_programs, - load_demo_sql_replay_entities, + open_demo_store_diag_window, + load_demo_store_diag, + open_demo_store_raw_window, + load_demo_store_raw, + open_demo_store_core_window, + load_demo_store_core, + open_demo_store_replay_candidates_window, + export_demo_store_replay_csv, + demo_store_replay_options, + load_demo_store_replay_transactions, + load_demo_store_replay_programs, + load_demo_store_replay_entities, open_demo_execution_solana_core_window, demo_execution_devnet_prepare_profile, demo_execution_solana_core_options, @@ -518,111 +518,111 @@ async fn demo_decode_replay_annotations( } #[tauri::command] -fn open_demo_sql_diag_window( +fn open_demo_store_diag_window( app_handle: tauri::AppHandle, ) -> std::result::Result<(), std::string::String> { return open_or_focus_demo_window( app_handle, - "demo_sql_diag", - "demo_sql_diag.html", - "SQL diagnostics", + "demo_store_diag", + "demo_store_diag.html", + "Store diagnostics", ); } #[tauri::command] -async fn load_demo_sql_diag( +async fn load_demo_store_diag( state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - return crate::load_demo_sql_diag(state).await; +) -> std::result::Result { + return crate::load_demo_store_diag(state).await; } #[tauri::command] -fn open_demo_sql_pg_raw_window( +fn open_demo_store_raw_window( app_handle: tauri::AppHandle, ) -> std::result::Result<(), std::string::String> { return open_or_focus_demo_window( app_handle, - "demo_sql_pg_raw", - "demo_sql_pg_raw.html", - "PostgreSQL canonical acquisition", + "demo_store_raw", + "demo_store_raw.html", + "Raw store", ); } #[tauri::command] -async fn load_demo_sql_pg_raw( +async fn load_demo_store_raw( state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - return crate::load_demo_sql_pg_raw(state).await; +) -> std::result::Result { + return crate::load_demo_store_raw(state).await; } #[tauri::command] -fn open_demo_sql_pg_core_window( +fn open_demo_store_core_window( app_handle: tauri::AppHandle, ) -> std::result::Result<(), std::string::String> { return open_or_focus_demo_window( app_handle, - "demo_sql_pg_core", - "demo_sql_pg_core.html", - "PostgreSQL core store", + "demo_store_core", + "demo_store_core.html", + "Core store", ); } #[tauri::command] -async fn load_demo_sql_pg_core( +async fn load_demo_store_core( state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - return crate::load_demo_sql_pg_core(state).await; +) -> std::result::Result { + return crate::load_demo_store_core(state).await; } #[tauri::command] -fn open_demo_sql_replay_candidates_window( +fn open_demo_store_replay_candidates_window( app_handle: tauri::AppHandle, ) -> std::result::Result<(), std::string::String> { return open_or_focus_demo_window( app_handle, - "demo_sql_replay_candidates", - "demo_sql_replay_candidates.html", - "SQL replay candidates", + "demo_store_replay_candidates", + "demo_store_replay_candidates.html", + "Store replay candidates", ); } #[tauri::command] -async fn export_demo_sql_replay_csv( +async fn export_demo_store_replay_csv( file_name: std::string::String, content: std::string::String, ) -> std::result::Result { - return crate::export_demo_sql_replay_csv(file_name, content).await; + return crate::export_demo_store_replay_csv(file_name, content).await; } #[tauri::command] -async fn demo_sql_replay_options( +async fn demo_store_replay_options( state: tauri::State<'_, crate::AppState>, -) -> std::result::Result { - return crate::demo_sql_replay_options(state).await; +) -> std::result::Result { + return crate::demo_store_replay_options(state).await; } #[tauri::command] -async fn load_demo_sql_replay_transactions( +async fn load_demo_store_replay_transactions( state: tauri::State<'_, crate::AppState>, - request: crate::DemoSqlReplayTransactionRequest, -) -> std::result::Result, std::string::String> { - return crate::load_demo_sql_replay_transactions(state, request).await; + request: crate::DemoStoreReplayTransactionRequest, +) -> std::result::Result, std::string::String> { + return crate::load_demo_store_replay_transactions(state, request).await; } #[tauri::command] -async fn load_demo_sql_replay_programs( +async fn load_demo_store_replay_programs( state: tauri::State<'_, crate::AppState>, - request: crate::DemoSqlReplayProgramRequest, -) -> std::result::Result, std::string::String> { - return crate::load_demo_sql_replay_programs(state, request).await; + request: crate::DemoStoreReplayProgramRequest, +) -> std::result::Result, std::string::String> { + return crate::load_demo_store_replay_programs(state, request).await; } #[tauri::command] -async fn load_demo_sql_replay_entities( +async fn load_demo_store_replay_entities( state: tauri::State<'_, crate::AppState>, - request: crate::DemoSqlReplayEntityRequest, -) -> std::result::Result, std::string::String> { - return crate::load_demo_sql_replay_entities(state, request).await; + request: crate::DemoStoreReplayEntityRequest, +) -> std::result::Result, std::string::String> { + return crate::load_demo_store_replay_entities(state, request).await; } #[tauri::command] diff --git a/kb-app-demo-desktop/tauri.conf.json b/kb-app-demo-desktop/tauri.conf.json index 794e19f..45856d3 100644 --- a/kb-app-demo-desktop/tauri.conf.json +++ b/kb-app-demo-desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Khadhroony Bot3 Demo Desktop", - "version": "0.5.2", + "version": "0.5.3", "identifier": "com.sasedev.kb-app-demo-desktop", "build": { "beforeDevCommand": "npm run dev", diff --git a/kb-app-demo-desktop/vite.config.ts b/kb-app-demo-desktop/vite.config.ts index 2ba5679..c07b3b0 100644 --- a/kb-app-demo-desktop/vite.config.ts +++ b/kb-app-demo-desktop/vite.config.ts @@ -1,5 +1,5 @@ // file: kb-app-demo-desktop/vite.config.ts -// version: 10 +// version: 11 import { defineConfig, normalizePath } from "vite"; import { NodePackageImporter } from "sass-embedded"; @@ -30,10 +30,10 @@ export default defineConfig(() => ({ "demo_backfill": normalizePath(resolve(__dirname, 'frontend/demo_backfill.html')), "demo_core_extraction": normalizePath(resolve(__dirname, 'frontend/demo_core_extraction.html')), "demo_decode_replay": normalizePath(resolve(__dirname, 'frontend/demo_decode_replay.html')), - "demo_sql_diag": normalizePath(resolve(__dirname, 'frontend/demo_sql_diag.html')), - "demo_sql_pg_raw": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_raw.html')), - "demo_sql_pg_core": normalizePath(resolve(__dirname, 'frontend/demo_sql_pg_core.html')), - "demo_sql_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_sql_replay_candidates.html')), + "demo_store_diag": normalizePath(resolve(__dirname, 'frontend/demo_store_diag.html')), + "demo_store_raw": normalizePath(resolve(__dirname, 'frontend/demo_store_raw.html')), + "demo_store_core": normalizePath(resolve(__dirname, 'frontend/demo_store_core.html')), + "demo_store_replay_candidates": normalizePath(resolve(__dirname, 'frontend/demo_store_replay_candidates.html')), "demo_execution_solana_core": normalizePath(resolve(__dirname, 'frontend/demo_execution_solana_core.html')), "demo_execution_spl": normalizePath(resolve(__dirname, 'frontend/demo_execution_spl.html')), "demo_execution_metatdata": normalizePath(resolve(__dirname, 'frontend/demo_execution_metadata.html')), diff --git a/ks-config/CHANGELOG.md b/ks-config/CHANGELOG.md index 2a72ba0..8373c69 100644 --- a/ks-config/CHANGELOG.md +++ b/ks-config/CHANGELOG.md @@ -1,8 +1,19 @@ - + # CHANGELOG — ks-config +## `0.5.3-pre.002-delta-fix-001` + +- aligne tous les contrats/types publics de `ks-config` sur la règle workspace imposant `crate::Item` depuis leur crate propriétaire, y compris dans leur module de déclaration et dans les tests ; +- aligne les appels aux fonctions publiques réexportées sur le crate-root et ajoute le canari `KH_CFG015` pour empêcher le retour d'une référence locale non qualifiée à un type public de configuration. + +## `0.5.3-pre.002` + +- remplace les contrats runtime `PostgresConfig`/`SqliteConfig` par `DatabaseConfig { enabled, backend, backend_options }` afin que `ks-config` ne connaisse plus les paramètres d'un moteur non sélectionné ; +- limite la validation store à la structure backend-agnostique et laisse `ks-store` interpréter/valider les options concrètes, sans modifier les conventions `KS_SECRET_*` ni la résolution des placeholders ; +- aligne `store.config.json`, son exemple, le schéma spécialisé, le schéma runtime et les fixtures de configuration sur ce contrat opaque. + ## `0.5.2-pre.006-delta-fix-007` - corrige le test de séparation des répertoires wallet afin qu’il résolve d’abord `${KS_WALLETS_DIRECTORY:-wallets}` comme le chemin de chargement runtime ; diff --git a/ks-config/README.md b/ks-config/README.md index 4accd05..156b025 100644 --- a/ks-config/README.md +++ b/ks-config/README.md @@ -1,5 +1,5 @@ - + # ks-config @@ -59,6 +59,8 @@ Les valeurs globales telles que `logs_directory` et `wallets_directory` ne sont Depuis `0.5.1`, `ks-config` ne dépend plus de TS-RS et ne produit plus de bindings TypeScript. Ses contrats source/runtime peuvent contenir des secrets résolus : ils ne dérivent donc ni `serde::Serialize` ni `Debug`. Les applications propriétaires construisent leurs DTO publics et diagnostics bornés champ par champ. +Depuis `0.5.3-pre.002`, `DatabaseConfig` ne modélise plus PostgreSQL/SQLite : il conserve uniquement `enabled`, le code `backend` et un objet `backend_options` opaque. `ks-config` charge, compose et résout les placeholders ; la validation des paramètres du moteur et la création de la connexion appartiennent exclusivement à `ks-store`. + La section `application` d'une composition est volontairement opaque pour `ks-config`. Chaque binaire propriétaire définit son schéma et le valide avec `validate_json_value_against_schema`; le desktop utilise `config/schemas/kb-app-demo-desktop.application.config.schema.json`. ## Documents diff --git a/ks-config/USAGE.md b/ks-config/USAGE.md index 3a8b272..0534d19 100644 --- a/ks-config/USAGE.md +++ b/ks-config/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ks-config @@ -88,7 +88,7 @@ Les endpoints WebSocket source sélectionnent une classe `ws_endpoint_defaults`; ## Store -`store.config.json` possède les paramètres de backend et les profils PostgreSQL/SQLite. Les helpers principaux sont `read_store_json_file_with_environment`, `default_store_profile` et `store_profile`. +`store.config.json` sélectionne un backend par profil et transporte ses paramètres sous `backend_options`. Ce bloc reste opaque pour `ks-config` : seuls le code backend non vide et la forme objet sont validés ici. Les helpers principaux sont `read_store_json_file_with_environment`, `default_store_profile` et `store_profile`; `ks-store` interprète ensuite les options du backend sélectionné et crée la connexion. ## Wallet diff --git a/ks-config/src/composition.rs b/ks-config/src/composition.rs index ca6e2c7..11eaa2e 100644 --- a/ks-config/src/composition.rs +++ b/ks-config/src/composition.rs @@ -1,5 +1,5 @@ // file: ks-config/src/composition.rs -// version: 3 +// version: 5 //! Binary composition configuration and resolution into the shared runtime profile contract. @@ -18,9 +18,9 @@ pub struct CompositionConfigDocument { /// Active binary composition profile name. pub active_profile: std::string::String, /// Shared configuration documents referenced by this composition. - pub sources: CompositionConfigSources, + pub sources: crate::CompositionConfigSources, /// Named binary composition profiles. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Paths of shared configuration documents consumed by one binary composition. @@ -65,7 +65,7 @@ pub struct CompositionProfileConfig { #[derive(Clone, Eq, PartialEq)] pub struct ComposedAppConfig { /// Parsed binary composition document. - pub composition: CompositionConfigDocument, + pub composition: crate::CompositionConfigDocument, /// Resolved runtime application configuration. pub app_config: crate::AppConfig, /// Resolved logging source path. @@ -101,7 +101,7 @@ pub fn composition_json_schema_value() -> ks_core::Result { /// Validates raw binary composition JSON against its embedded schema. pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> { - let schema = match composition_json_schema_value() { + let schema = match crate::composition_json_schema_value() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -133,12 +133,12 @@ pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> { } /// Parses and validates one binary composition document. -pub fn parse_composition_json(raw_json: &str) -> ks_core::Result { - match validate_composition_json_schema(raw_json) { +pub fn parse_composition_json(raw_json: &str) -> ks_core::Result { + match crate::validate_composition_json_schema(raw_json) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - let document = match serde_json::from_str::(raw_json) { + let document = match serde_json::from_str::(raw_json) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -147,7 +147,7 @@ pub fn parse_composition_json(raw_json: &str) -> ks_core::Result std::result::Result::Ok(document), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -156,7 +156,7 @@ pub fn parse_composition_json(raw_json: &str) -> ks_core::Result ks_core::Result { +) -> ks_core::Result { let raw_json = match std::fs::read_to_string(path) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -166,14 +166,14 @@ pub fn read_composition_json_file( )); }, }; - return parse_composition_json(&raw_json); + return crate::parse_composition_json(&raw_json); } /// Loads workspace environment values, resolves placeholders and parses one binary composition document. pub fn read_composition_json_file_with_environment( path: &std::path::Path, workspace_root: &std::path::Path, -) -> ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -187,14 +187,14 @@ pub fn read_composition_json_file_with_environment( )); }, }; - return parse_composition_json(&crate::resolve_environment_placeholders(&raw_json)); + return crate::parse_composition_json(&crate::resolve_environment_placeholders(&raw_json)); } /// Returns one named binary composition profile. pub fn composition_profile<'a>( - document: &'a CompositionConfigDocument, + document: &'a crate::CompositionConfigDocument, profile_name: &str, -) -> ks_core::Result<&'a CompositionProfileConfig> { +) -> ks_core::Result<&'a crate::CompositionProfileConfig> { for profile in &document.profiles { if profile.name == profile_name { return std::result::Result::Ok(profile); @@ -208,9 +208,9 @@ pub fn composition_profile<'a>( /// Returns the active binary composition profile. pub fn active_composition_profile( - document: &CompositionConfigDocument, -) -> ks_core::Result<&CompositionProfileConfig> { - return composition_profile(document, document.active_profile.as_str()); + document: &crate::CompositionConfigDocument, +) -> ks_core::Result<&crate::CompositionProfileConfig> { + return crate::composition_profile(document, document.active_profile.as_str()); } /// Resolves a source path relative to the workspace root unless already absolute. @@ -238,7 +238,7 @@ pub fn resolve_profile_selection<'a>( /// Resolves all shared source selections into the runtime application contract. pub fn compose_app_config( - document: &CompositionConfigDocument, + document: &crate::CompositionConfigDocument, transport: &crate::TransportConfigDocument, listeners: &crate::ListenersConfigDocument, store: &crate::StoreConfigDocument, @@ -247,23 +247,23 @@ pub fn compose_app_config( ) -> ks_core::Result { let mut profiles = std::vec::Vec::::new(); for profile in &document.profiles { - let transport_name = resolve_profile_selection( + let transport_name = crate::resolve_profile_selection( profile.transport_profile.as_deref(), transport.default_profile.as_str(), ); - let listeners_name = resolve_profile_selection( + let listeners_name = crate::resolve_profile_selection( profile.listeners_profile.as_deref(), listeners.default_profile.as_str(), ); - let store_name = resolve_profile_selection( + let store_name = crate::resolve_profile_selection( profile.store_profile.as_deref(), store.default_profile.as_str(), ); - let wallet_name = resolve_profile_selection( + let wallet_name = crate::resolve_profile_selection( profile.wallet_profile.as_deref(), wallet.default_profile.as_str(), ); - let execution_name = resolve_profile_selection( + let execution_name = crate::resolve_profile_selection( profile.execution_profile.as_deref(), execution.default_profile.as_str(), ); @@ -313,24 +313,34 @@ pub fn compose_app_config( pub fn read_composed_app_config_with_environment( composition_path: &std::path::Path, workspace_root: &std::path::Path, -) -> ks_core::Result { - let composition = - match read_composition_json_file_with_environment(composition_path, workspace_root) { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let logging_path = - resolve_composition_source_path(workspace_root, composition.sources.logging.as_str()); - let transport_path = - resolve_composition_source_path(workspace_root, composition.sources.transport.as_str()); - let listeners_path = - resolve_composition_source_path(workspace_root, composition.sources.listeners.as_str()); +) -> ks_core::Result { + let composition = match crate::read_composition_json_file_with_environment( + composition_path, + workspace_root, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let logging_path = crate::resolve_composition_source_path( + workspace_root, + composition.sources.logging.as_str(), + ); + let transport_path = crate::resolve_composition_source_path( + workspace_root, + composition.sources.transport.as_str(), + ); + let listeners_path = crate::resolve_composition_source_path( + workspace_root, + composition.sources.listeners.as_str(), + ); let store_path = - resolve_composition_source_path(workspace_root, composition.sources.store.as_str()); + crate::resolve_composition_source_path(workspace_root, composition.sources.store.as_str()); let wallet_path = - resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str()); - let execution_path = - resolve_composition_source_path(workspace_root, composition.sources.execution.as_str()); + crate::resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str()); + let execution_path = crate::resolve_composition_source_path( + workspace_root, + composition.sources.execution.as_str(), + ); let transport = match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) { std::result::Result::Ok(value) => value, @@ -359,13 +369,18 @@ pub fn read_composed_app_config_with_environment( std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - let app_config = - match compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution) - { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - return std::result::Result::Ok(ComposedAppConfig { + let app_config = match crate::compose_app_config( + &composition, + &transport, + &listeners, + &store, + &wallet, + &execution, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(crate::ComposedAppConfig { composition, app_config, logging_path, @@ -421,9 +436,9 @@ pub fn read_default_shared_app_config_with_environment( } else { "shared_default".to_string() }; - let composition = CompositionConfigDocument { + let composition = crate::CompositionConfigDocument { active_profile: profile_name.clone(), - sources: CompositionConfigSources { + sources: crate::CompositionConfigSources { logging: DEFAULT_LOGGING_CONFIG_PATH.to_string(), transport: DEFAULT_TRANSPORT_CONFIG_PATH.to_string(), listeners: DEFAULT_LISTENERS_CONFIG_PATH.to_string(), @@ -431,7 +446,7 @@ pub fn read_default_shared_app_config_with_environment( wallet: DEFAULT_WALLET_CONFIG_PATH.to_string(), execution: DEFAULT_EXECUTION_CONFIG_PATH.to_string(), }, - profiles: vec![CompositionProfileConfig { + profiles: vec![crate::CompositionProfileConfig { name: profile_name.clone(), application: std::option::Option::None, logging_profile: std::option::Option::None, @@ -442,11 +457,20 @@ pub fn read_default_shared_app_config_with_environment( execution_profile: std::option::Option::None, }], }; - return compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution); + return crate::compose_app_config( + &composition, + &transport, + &listeners, + &store, + &wallet, + &execution, + ); } /// Validates a typed binary composition document before referenced documents are loaded. -pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks_core::Result<()> { +pub fn validate_composition_document( + document: &crate::CompositionConfigDocument, +) -> ks_core::Result<()> { match crate::require_non_empty(&document.active_profile, "composition.active_profile") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -498,7 +522,7 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks )); } } - return match active_composition_profile(document) { + return match crate::active_composition_profile(document) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -507,7 +531,7 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks fn validate_logging_references_with_environment( path: &std::path::Path, workspace_root: &std::path::Path, - composition: &CompositionConfigDocument, + composition: &crate::CompositionConfigDocument, ) -> ks_core::Result<()> { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), @@ -566,7 +590,7 @@ fn validate_logging_references_with_environment( } for profile in &composition.profiles { let selected = - resolve_profile_selection(profile.logging_profile.as_deref(), default_profile); + crate::resolve_profile_selection(profile.logging_profile.as_deref(), default_profile); if !names.contains(selected) { return std::result::Result::Err(ks_core::Error::new( "composition_logging_profile_not_found", @@ -586,17 +610,17 @@ mod tests { #[test] fn default_and_example_compositions_validate() { - assert!(super::parse_composition_json(DEFAULT_COMPOSITION).is_ok()); - assert!(super::parse_composition_json(EXAMPLE_COMPOSITION).is_ok()); + assert!(crate::parse_composition_json(DEFAULT_COMPOSITION).is_ok()); + assert!(crate::parse_composition_json(EXAMPLE_COMPOSITION).is_ok()); } #[test] fn local_devnet_composition_uses_shared_defaults() { - let composition = match super::parse_composition_json(DEFAULT_COMPOSITION) { + let composition = match crate::parse_composition_json(DEFAULT_COMPOSITION) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("default composition must parse: {error}"), }; - let profile = match super::composition_profile(&composition, "local_devnet") { + let profile = match crate::composition_profile(&composition, "local_devnet") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("local Devnet composition must resolve: {error}") diff --git a/ks-config/src/environment.rs b/ks-config/src/environment.rs index a88e315..5271f3e 100644 --- a/ks-config/src/environment.rs +++ b/ks-config/src/environment.rs @@ -1,5 +1,5 @@ // file: ks-config/src/environment.rs -// version: 6 +// version: 8 //! Environment-file loading and configuration placeholder resolution. @@ -16,7 +16,7 @@ pub struct EnvironmentLoadReport { /// fallback text declared with `${NAME:-fallback}` placeholders. pub fn load_workspace_environment( workspace_root: &std::path::Path, -) -> ks_core::Result { +) -> ks_core::Result { let explicit_path = std::env::var("KS_ENV_FILE").ok(); let selected_path = match explicit_path { std::option::Option::Some(path) if !path.trim().is_empty() => { @@ -26,10 +26,10 @@ pub fn load_workspace_environment( _ => workspace_root.join(".env"), }; if !selected_path.exists() { - return std::result::Result::Ok(EnvironmentLoadReport { loaded_path: None }); + return std::result::Result::Ok(crate::EnvironmentLoadReport { loaded_path: None }); } return match dotenvy::from_path(&selected_path) { - std::result::Result::Ok(()) => std::result::Result::Ok(EnvironmentLoadReport { + std::result::Result::Ok(()) => std::result::Result::Ok(crate::EnvironmentLoadReport { loaded_path: std::option::Option::Some(selected_path), }), std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new( @@ -82,14 +82,14 @@ mod tests { #[test] fn fallback_is_used_when_variable_is_absent() { let resolved = - super::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}"); + crate::resolve_environment_placeholders("${KS_CONFIG_TEST_MISSING:-fallback}"); assert_eq!(resolved, "fallback"); } #[test] fn unresolved_required_placeholder_is_preserved() { let resolved = - super::resolve_environment_placeholders("prefix-${KS_CONFIG_TEST_MISSING}-suffix"); + crate::resolve_environment_placeholders("prefix-${KS_CONFIG_TEST_MISSING}-suffix"); assert_eq!(resolved, "prefix-${KS_CONFIG_TEST_MISSING}-suffix"); } } diff --git a/ks-config/src/execution.rs b/ks-config/src/execution.rs index 28e3c77..9db28a7 100644 --- a/ks-config/src/execution.rs +++ b/ks-config/src/execution.rs @@ -1,5 +1,5 @@ // file: ks-config/src/execution.rs -// version: 3 +// version: 5 //! Independent execution policy configuration document loading and validation. @@ -12,7 +12,7 @@ pub struct ExecutionConfigDocument { /// Default execution profile used when a composition does not override it. pub default_profile: std::string::String, /// Named execution policy profiles. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Named execution policy profile. @@ -31,7 +31,7 @@ pub fn execution_json_schema_text() -> &'static str { } /// Parses and validates one execution configuration document. -pub fn parse_execution_json(raw_json: &str) -> ks_core::Result { +pub fn parse_execution_json(raw_json: &str) -> ks_core::Result { let schema = match serde_json::from_str::(EXECUTION_JSON_SCHEMA) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -65,7 +65,7 @@ pub fn parse_execution_json(raw_json: &str) -> ks_core::Result(raw_json) { + let document = match serde_json::from_str::(raw_json) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -74,7 +74,7 @@ pub fn parse_execution_json(raw_json: &str) -> ks_core::Result std::result::Result::Ok(document), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -84,7 +84,7 @@ pub fn parse_execution_json(raw_json: &str) -> ks_core::Result ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -98,14 +98,14 @@ pub fn read_execution_json_file_with_environment( )); }, }; - return parse_execution_json(&crate::resolve_environment_placeholders(&raw_json)); + return crate::parse_execution_json(&crate::resolve_environment_placeholders(&raw_json)); } /// Returns one named execution profile. pub fn execution_profile<'a>( - document: &'a ExecutionConfigDocument, + document: &'a crate::ExecutionConfigDocument, profile_name: &str, -) -> ks_core::Result<&'a ExecutionProfileConfig> { +) -> ks_core::Result<&'a crate::ExecutionProfileConfig> { for profile in &document.profiles { if profile.name == profile_name { return std::result::Result::Ok(profile); @@ -119,13 +119,15 @@ pub fn execution_profile<'a>( /// Returns the default execution profile. pub fn default_execution_profile( - document: &ExecutionConfigDocument, -) -> ks_core::Result<&ExecutionProfileConfig> { - return execution_profile(document, document.default_profile.as_str()); + document: &crate::ExecutionConfigDocument, +) -> ks_core::Result<&crate::ExecutionProfileConfig> { + return crate::execution_profile(document, document.default_profile.as_str()); } /// Validates one typed execution configuration document. -pub fn validate_execution_document(document: &ExecutionConfigDocument) -> ks_core::Result<()> { +pub fn validate_execution_document( + document: &crate::ExecutionConfigDocument, +) -> ks_core::Result<()> { match crate::require_non_empty(&document.default_profile, "execution.default_profile") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -153,7 +155,7 @@ pub fn validate_execution_document(document: &ExecutionConfigDocument) -> ks_cor std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return match default_execution_profile(document) { + return match crate::default_execution_profile(document) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -167,7 +169,7 @@ mod tests { #[test] fn default_and_example_execution_configs_validate() { - assert!(super::parse_execution_json(DEFAULT_EXECUTION).is_ok()); - assert!(super::parse_execution_json(EXAMPLE_EXECUTION).is_ok()); + assert!(crate::parse_execution_json(DEFAULT_EXECUTION).is_ok()); + assert!(crate::parse_execution_json(EXAMPLE_EXECUTION).is_ok()); } } diff --git a/ks-config/src/lib.rs b/ks-config/src/lib.rs index 463593a..e80fb7b 100644 --- a/ks-config/src/lib.rs +++ b/ks-config/src/lib.rs @@ -1,5 +1,5 @@ // file: ks-config/src/lib.rs -// version: 10 +// version: 11 //! Khadhroony Solana shared configuration contracts and composition helpers. #![warn(missing_docs)] @@ -125,16 +125,12 @@ pub use self::settings::HttpEndpointConfig; pub use self::settings::ListenerConfig; /// Exposes the log listener configuration type. pub use self::settings::LogListenerConfig; -/// Exposes the PostgreSQL configuration type. -pub use self::settings::PostgresConfig; /// Exposes the profile configuration type. pub use self::settings::ProfileConfig; /// Exposes the program listener configuration type. pub use self::settings::ProgramListenerConfig; /// Exposes the Solana configuration type. pub use self::settings::SolanaConfig; -/// Exposes the SQLite configuration type. -pub use self::settings::SqliteConfig; /// Exposes the wallet configuration type. pub use self::settings::WalletConfig; /// Exposes the WebSocket endpoint configuration type. diff --git a/ks-config/src/listeners.rs b/ks-config/src/listeners.rs index 78445a9..a14c713 100644 --- a/ks-config/src/listeners.rs +++ b/ks-config/src/listeners.rs @@ -1,5 +1,5 @@ // file: ks-config/src/listeners.rs -// version: 3 +// version: 5 //! Independent Solana listener configuration document loading and validation. @@ -12,7 +12,7 @@ pub struct ListenersConfigDocument { /// Default listener profile used by consumers that do not provide an explicit selection. pub default_profile: std::string::String, /// Named listener profiles available in this document. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Named listener profile. @@ -51,7 +51,7 @@ pub fn listeners_json_schema_value() -> ks_core::Result { /// Validates raw listener JSON against the embedded listener schema. pub fn validate_listeners_json_schema(raw_json: &str) -> ks_core::Result<()> { - let schema = match listeners_json_schema_value() { + let schema = match crate::listeners_json_schema_value() { std::result::Result::Ok(schema) => schema, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -83,12 +83,12 @@ pub fn validate_listeners_json_schema(raw_json: &str) -> ks_core::Result<()> { } /// Parses and validates one independent listener configuration document. -pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result { - match validate_listeners_json_schema(raw_json) { +pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result { + match crate::validate_listeners_json_schema(raw_json) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - let document = match serde_json::from_str::(raw_json) { + let document = match serde_json::from_str::(raw_json) { std::result::Result::Ok(document) => document, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -97,7 +97,7 @@ pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result std::result::Result::Ok(document), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -106,7 +106,7 @@ pub fn parse_listeners_json(raw_json: &str) -> ks_core::Result ks_core::Result { +) -> ks_core::Result { let raw_json = match std::fs::read_to_string(path) { std::result::Result::Ok(content) => content, std::result::Result::Err(error) => { @@ -116,14 +116,14 @@ pub fn read_listeners_json_file( )); }, }; - return parse_listeners_json(&raw_json); + return crate::parse_listeners_json(&raw_json); } /// Loads workspace environment values, resolves placeholders and parses listener configuration. pub fn read_listeners_json_file_with_environment( path: &std::path::Path, workspace_root: &std::path::Path, -) -> ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -138,14 +138,14 @@ pub fn read_listeners_json_file_with_environment( }, }; let resolved = crate::resolve_environment_placeholders(&raw_json); - return parse_listeners_json(&resolved); + return crate::parse_listeners_json(&resolved); } /// Returns one named listener profile. pub fn listeners_profile<'a>( - document: &'a ListenersConfigDocument, + document: &'a crate::ListenersConfigDocument, profile_name: &str, -) -> ks_core::Result<&'a ListenersProfileConfig> { +) -> ks_core::Result<&'a crate::ListenersProfileConfig> { for profile in &document.profiles { if profile.name == profile_name { return std::result::Result::Ok(profile); @@ -159,13 +159,13 @@ pub fn listeners_profile<'a>( /// Returns the default listener profile declared by the document. pub fn default_listeners_profile( - document: &ListenersConfigDocument, -) -> ks_core::Result<&ListenersProfileConfig> { - return listeners_profile(document, document.default_profile.as_str()); + document: &crate::ListenersConfigDocument, +) -> ks_core::Result<&crate::ListenersProfileConfig> { + return crate::listeners_profile(document, document.default_profile.as_str()); } /// Converts one named listener profile into the resolved runtime listener contract. -pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::ListenerConfig { +pub fn resolved_listener_config(profile: &crate::ListenersProfileConfig) -> crate::ListenerConfig { return crate::ListenerConfig { enabled: profile.enabled, default_commitment: profile.default_commitment.clone(), @@ -176,7 +176,9 @@ pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::List } /// Validates a typed listener configuration document. -pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_core::Result<()> { +pub fn validate_listeners_document( + document: &crate::ListenersConfigDocument, +) -> ks_core::Result<()> { match crate::require_non_empty(&document.default_profile, "default_profile") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -199,13 +201,13 @@ pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_cor profile.name.clone(), )); } - let resolved = resolved_listener_config(profile); + let resolved = crate::resolved_listener_config(profile); match crate::validate_listeners(&resolved) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return match default_listeners_profile(document) { + return match crate::default_listeners_profile(document) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -219,7 +221,7 @@ mod tests { #[test] fn default_and_example_listener_configs_validate() { - assert!(super::parse_listeners_json(DEFAULT_LISTENERS).is_ok()); - assert!(super::parse_listeners_json(EXAMPLE_LISTENERS).is_ok()); + assert!(crate::parse_listeners_json(DEFAULT_LISTENERS).is_ok()); + assert!(crate::parse_listeners_json(EXAMPLE_LISTENERS).is_ok()); } } diff --git a/ks-config/src/schema.rs b/ks-config/src/schema.rs index 2e6f970..4caac69 100644 --- a/ks-config/src/schema.rs +++ b/ks-config/src/schema.rs @@ -1,5 +1,5 @@ // file: ks-config/src/schema.rs -// version: 1 +// version: 2 //! Generic JSON Schema validation helpers for binary-owned configuration fragments. @@ -42,7 +42,7 @@ mod tests { fn validation_error_never_echoes_instance_secret() { let schema = r#"{"type":"object","additionalProperties":false,"required":["value"],"properties":{"value":{"type":"integer"}}}"#; let instance = serde_json::json!({"value": "SECRET-CANARY"}); - let result = super::validate_json_value_against_schema( + let result = crate::validate_json_value_against_schema( schema, &instance, "test_fragment_validation_failed", diff --git a/ks-config/src/sensitivity.rs b/ks-config/src/sensitivity.rs index c35a9cd..eceb968 100644 --- a/ks-config/src/sensitivity.rs +++ b/ks-config/src/sensitivity.rs @@ -1,5 +1,5 @@ // file: ks-config/src/sensitivity.rs -// version: 1 +// version: 3 //! Application environment-variable sensitivity classification. @@ -20,7 +20,7 @@ pub enum EnvironmentValueSensitivity { /// one `KS_SECRET_*` placeholder is classified as secret even when the surrounding URL is public. pub fn classify_environment_template( raw: &str, -) -> std::option::Option { +) -> std::option::Option { let bytes = raw.as_bytes(); let mut sensitivity = std::option::Option::None; let mut index = 0_usize; @@ -36,7 +36,8 @@ pub fn classify_environment_template( std::option::Option::Some((name, _)) => name, std::option::Option::None => expression, }; - sensitivity = merge_sensitivity(sensitivity, classify_environment_variable(name)); + sensitivity = + merge_sensitivity(sensitivity, crate::classify_environment_variable(name)); index = end + 1; continue; } @@ -49,15 +50,15 @@ pub fn classify_environment_template( /// Classifies a Khadhroony-owned environment variable by its namespace. pub fn classify_environment_variable( name: &str, -) -> std::option::Option { +) -> std::option::Option { if name.starts_with("KS_SECRET_") || name.starts_with("KB_SECRET_") { - return std::option::Option::Some(EnvironmentValueSensitivity::Secret); + return std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret); } if name.starts_with("KS_PUBLIC_") || name.starts_with("KB_PUBLIC_") { - return std::option::Option::Some(EnvironmentValueSensitivity::Public); + return std::option::Option::Some(crate::EnvironmentValueSensitivity::Public); } if name.starts_with("KS_") || name.starts_with("KB_") { - return std::option::Option::Some(EnvironmentValueSensitivity::Internal); + return std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal); } return std::option::Option::None; } @@ -67,14 +68,14 @@ pub fn diagnostic_environment_value( name: &str, value: &str, ) -> std::option::Option { - return match classify_environment_variable(name) { - std::option::Option::Some(EnvironmentValueSensitivity::Secret) => { + return match crate::classify_environment_variable(name) { + std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret) => { std::option::Option::Some("".to_string()) }, - std::option::Option::Some(EnvironmentValueSensitivity::Internal) => { + std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal) => { std::option::Option::Some("".to_string()) }, - std::option::Option::Some(EnvironmentValueSensitivity::Public) => { + std::option::Option::Some(crate::EnvironmentValueSensitivity::Public) => { std::option::Option::Some(value.to_string()) }, std::option::Option::None => std::option::Option::None, @@ -82,23 +83,23 @@ pub fn diagnostic_environment_value( } fn merge_sensitivity( - left: std::option::Option, - right: std::option::Option, -) -> std::option::Option { - if left == std::option::Option::Some(EnvironmentValueSensitivity::Secret) - || right == std::option::Option::Some(EnvironmentValueSensitivity::Secret) + left: std::option::Option, + right: std::option::Option, +) -> std::option::Option { + if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret) + || right == std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret) { - return std::option::Option::Some(EnvironmentValueSensitivity::Secret); + return std::option::Option::Some(crate::EnvironmentValueSensitivity::Secret); } - if left == std::option::Option::Some(EnvironmentValueSensitivity::Internal) - || right == std::option::Option::Some(EnvironmentValueSensitivity::Internal) + if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal) + || right == std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal) { - return std::option::Option::Some(EnvironmentValueSensitivity::Internal); + return std::option::Option::Some(crate::EnvironmentValueSensitivity::Internal); } - if left == std::option::Option::Some(EnvironmentValueSensitivity::Public) - || right == std::option::Option::Some(EnvironmentValueSensitivity::Public) + if left == std::option::Option::Some(crate::EnvironmentValueSensitivity::Public) + || right == std::option::Option::Some(crate::EnvironmentValueSensitivity::Public) { - return std::option::Option::Some(EnvironmentValueSensitivity::Public); + return std::option::Option::Some(crate::EnvironmentValueSensitivity::Public); } return std::option::Option::None; } @@ -108,19 +109,19 @@ mod tests { #[test] fn composed_values_inherit_the_strongest_placeholder_sensitivity() { assert_eq!( - super::classify_environment_template( + crate::classify_environment_template( "https://rpc.invalid/?api-key=${KS_SECRET_HELIUS_API_KEY}&mint=${KS_PUBLIC_DEVNET_TOKEN_2022_MINT}" ), std::option::Option::Some(super::EnvironmentValueSensitivity::Secret) ); assert_eq!( - super::classify_environment_template( + crate::classify_environment_template( "${KS_LOGS_DIRECTORY:-logs}/${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}" ), std::option::Option::Some(super::EnvironmentValueSensitivity::Internal) ); assert_eq!( - super::classify_environment_template("${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"), + crate::classify_environment_template("${KS_PUBLIC_SPL_TOKEN_PROGRAM_ID}"), std::option::Option::Some(super::EnvironmentValueSensitivity::Public) ); } @@ -128,15 +129,15 @@ mod tests { #[test] fn generic_diagnostic_never_reveals_secret_or_internal_values() { assert_eq!( - super::diagnostic_environment_value("KS_SECRET_HELIUS_API_KEY", "SECRET-CANARY"), + crate::diagnostic_environment_value("KS_SECRET_HELIUS_API_KEY", "SECRET-CANARY"), std::option::Option::Some("".to_string()) ); assert_eq!( - super::diagnostic_environment_value("KS_LOGS_DIRECTORY", "/private/logs"), + crate::diagnostic_environment_value("KS_LOGS_DIRECTORY", "/private/logs"), std::option::Option::Some("".to_string()) ); assert_eq!( - super::diagnostic_environment_value("KS_PUBLIC_SPL_TOKEN_PROGRAM_ID", "Token111"), + crate::diagnostic_environment_value("KS_PUBLIC_SPL_TOKEN_PROGRAM_ID", "Token111"), std::option::Option::Some("Token111".to_string()) ); } @@ -144,17 +145,17 @@ mod tests { #[test] fn namespaces_map_to_secret_internal_and_public() { assert_eq!( - super::classify_environment_variable("KS_SECRET_HELIUS_API_KEY"), + crate::classify_environment_variable("KS_SECRET_HELIUS_API_KEY"), std::option::Option::Some(super::EnvironmentValueSensitivity::Secret) ); assert_eq!( - super::classify_environment_variable("KS_LOGS_DIRECTORY"), + crate::classify_environment_variable("KS_LOGS_DIRECTORY"), std::option::Option::Some(super::EnvironmentValueSensitivity::Internal) ); assert_eq!( - super::classify_environment_variable("KB_PUBLIC_DEMO_VALUE"), + crate::classify_environment_variable("KB_PUBLIC_DEMO_VALUE"), std::option::Option::Some(super::EnvironmentValueSensitivity::Public) ); - assert_eq!(super::classify_environment_variable("HOME"), std::option::Option::None); + assert_eq!(crate::classify_environment_variable("HOME"), std::option::Option::None); } } diff --git a/ks-config/src/settings.rs b/ks-config/src/settings.rs index 615c707..9f04939 100644 --- a/ks-config/src/settings.rs +++ b/ks-config/src/settings.rs @@ -1,5 +1,5 @@ // file: ks-config/src/settings.rs -// version: 25 +// version: 29 //! Resolved runtime configuration models retained during the `0.5.1` source-document split. @@ -12,7 +12,7 @@ pub struct AppConfig { /// Active profile name. pub active_profile: std::string::String, /// Named profiles reconstructed from the active binary composition and shared documents. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Resolved runtime profile selected by the root active profile name. @@ -21,67 +21,35 @@ pub struct ProfileConfig { /// Profile code. pub name: std::string::String, /// Database configuration. - pub database: DatabaseConfig, + pub database: crate::DatabaseConfig, /// Solana endpoint and listener configuration. - pub solana: SolanaConfig, + pub solana: crate::SolanaConfig, /// Wallet configuration. - pub wallet: WalletConfig, + pub wallet: crate::WalletConfig, /// Execution safety configuration. - pub execution: ExecutionConfig, + pub execution: crate::ExecutionConfig, } -/// Database backend configuration. +/// Backend-agnostic store configuration resolved by `ks-config`. #[derive(Clone, serde::Deserialize, Eq, PartialEq)] pub struct DatabaseConfig { - /// Enables database storage. + /// Enables persistent storage for this profile. pub enabled: bool, - /// Selected backend code. + /// Selected backend code interpreted by `ks-store`. pub backend: std::string::String, - /// PostgreSQL configuration. - pub postgres: PostgresConfig, - /// SQLite configuration for tests or imports. - pub sqlite: SqliteConfig, -} - -/// PostgreSQL backend configuration. -#[derive(Clone, serde::Deserialize, Eq, PartialEq)] -pub struct PostgresConfig { - /// PostgreSQL URL or local placeholder. - pub url: std::string::String, - /// Maximum connection count. - pub max_connections: u32, - /// Connection timeout in milliseconds. - pub connect_timeout_ms: u64, - /// Enables schema initialization at startup. - pub auto_initialize_schema: bool, -} - -/// SQLite backend configuration. -#[derive(Clone, serde::Deserialize, Eq, PartialEq)] -pub struct SqliteConfig { - /// SQLite database path. - pub path: std::string::String, - /// Creates the file when missing. - pub create_if_missing: bool, - /// Busy timeout in milliseconds. - pub busy_timeout_ms: u64, - /// Maximum connection count. - pub max_connections: u32, - /// Enables schema initialization at startup. - pub auto_initialize_schema: bool, - /// Enables WAL mode. - pub use_wal: bool, + /// Backend-specific options retained as an opaque JSON object until `ks-store` opens the store. + pub backend_options: serde_json::Value, } /// Solana endpoints and listener configuration. #[derive(Clone, serde::Deserialize, Eq, PartialEq)] pub struct SolanaConfig { /// HTTP JSON-RPC endpoints. - pub http_endpoints: std::vec::Vec, + pub http_endpoints: std::vec::Vec, /// Standard Solana WebSocket endpoints. - pub ws_endpoints: std::vec::Vec, + pub ws_endpoints: std::vec::Vec, /// Listener declarations. - pub listeners: ListenerConfig, + pub listeners: crate::ListenerConfig, } /// HTTP JSON-RPC endpoint configuration. @@ -104,7 +72,7 @@ pub struct HttpEndpointConfig { /// Maximum idle connections per host. pub max_idle_connections_per_host: u32, /// Role-specific limits for this URL. - pub roles: std::vec::Vec, + pub roles: std::vec::Vec, } /// Standard Solana WebSocket endpoint configuration. @@ -133,7 +101,7 @@ pub struct WsEndpointConfig { /// Enables automatic reconnect for this endpoint. pub auto_reconnect: bool, /// Role-specific limits for this URL. - pub roles: std::vec::Vec, + pub roles: std::vec::Vec, } /// Role-specific endpoint limits. @@ -167,11 +135,11 @@ pub struct ListenerConfig { /// Default commitment used by listener subscriptions. pub default_commitment: std::string::String, /// Log listeners filtered by program mentions. - pub log_listeners: std::vec::Vec, + pub log_listeners: std::vec::Vec, /// Program account listeners. - pub program_listeners: std::vec::Vec, + pub program_listeners: std::vec::Vec, /// Account listeners. - pub account_listeners: std::vec::Vec, + pub account_listeners: std::vec::Vec, } /// Log listener filtered by a program identifier mention. @@ -298,7 +266,7 @@ pub fn config_json_schema_value() -> ks_core::Result { /// Validates a raw resolved runtime JSON string against the embedded compatibility schema. pub fn validate_config_json_schema(raw_json: &str) -> ks_core::Result<()> { - let schema = match config_json_schema_value() { + let schema = match crate::config_json_schema_value() { std::result::Result::Ok(schema) => schema, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -331,12 +299,12 @@ pub fn validate_config_json_schema(raw_json: &str) -> ks_core::Result<()> { } /// Parses the resolved runtime application contract from a JSON string and validates it. -pub fn parse_config_json(raw_json: &str) -> ks_core::Result { - match validate_config_json_schema(raw_json) { +pub fn parse_config_json(raw_json: &str) -> ks_core::Result { + match crate::validate_config_json_schema(raw_json) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - let config = match serde_json::from_str::(raw_json) { + let config = match serde_json::from_str::(raw_json) { std::result::Result::Ok(config) => config, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -345,14 +313,14 @@ pub fn parse_config_json(raw_json: &str) -> ks_core::Result { )); }, }; - return match validate_config(&config) { + return match crate::validate_config(&config) { std::result::Result::Ok(()) => std::result::Result::Ok(config), std::result::Result::Err(error) => std::result::Result::Err(error), }; } /// Reads and parses a resolved runtime application snapshot from a filesystem path. -pub fn read_config_json_file(path: &std::path::Path) -> ks_core::Result { +pub fn read_config_json_file(path: &std::path::Path) -> ks_core::Result { let raw_json = match std::fs::read_to_string(path) { std::result::Result::Ok(content) => content, std::result::Result::Err(error) => { @@ -362,14 +330,14 @@ pub fn read_config_json_file(path: &std::path::Path) -> ks_core::Result ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -384,11 +352,11 @@ pub fn read_config_json_file_with_environment( }, }; let resolved = crate::resolve_environment_placeholders(&raw_json); - return parse_config_json(&resolved); + return crate::parse_config_json(&resolved); } /// Returns the active profile declared by the root configuration. -pub fn active_profile(config: &AppConfig) -> ks_core::Result<&ProfileConfig> { +pub fn active_profile(config: &crate::AppConfig) -> ks_core::Result<&crate::ProfileConfig> { for profile in &config.profiles { if profile.name == config.active_profile { return std::result::Result::Ok(profile); @@ -401,7 +369,7 @@ pub fn active_profile(config: &AppConfig) -> ks_core::Result<&ProfileConfig> { } /// Validates a typed configuration after JSON Schema validation and deserialization. -pub fn validate_config(config: &AppConfig) -> ks_core::Result<()> { +pub fn validate_config(config: &crate::AppConfig) -> ks_core::Result<()> { match validate_root_config(config) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -412,14 +380,14 @@ pub fn validate_config(config: &AppConfig) -> ks_core::Result<()> { std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return match active_profile(config) { + return match crate::active_profile(config) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; } -fn validate_root_config(config: &AppConfig) -> ks_core::Result<()> { - match require_non_empty(&config.active_profile, "active_profile") { +fn validate_root_config(config: &crate::AppConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.active_profile, "active_profile") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -432,7 +400,7 @@ fn validate_root_config(config: &AppConfig) -> ks_core::Result<()> { let mut names = std::collections::BTreeSet::::new(); let mut active_count = 0_u32; for profile in &config.profiles { - match require_non_empty(&profile.name, "profile.name") { + match crate::require_non_empty(&profile.name, "profile.name") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -455,8 +423,8 @@ fn validate_root_config(config: &AppConfig) -> ks_core::Result<()> { return std::result::Result::Ok(()); } -fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> { - match validate_database(&profile.database) { +fn validate_profile(profile: &crate::ProfileConfig) -> ks_core::Result<()> { + match crate::validate_database(&profile.database) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -464,44 +432,32 @@ fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match validate_wallet(&profile.wallet) { + match crate::validate_wallet(&profile.wallet) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match validate_execution(&profile.execution) { + match crate::validate_execution(&profile.execution) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } return validate_wallet_execution_pair(&profile.wallet, &profile.execution); } -pub(crate) fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> { - if config.backend != "postgres" && config.backend != "sqlite" { - return std::result::Result::Err(ks_core::Error::new( - "database_backend_invalid", - config.backend.clone(), - )); - } - match require_non_empty(&config.postgres.url, "database.postgres.url") { +pub(crate) fn validate_database(config: &crate::DatabaseConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.backend, "database.backend") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - if config.postgres.max_connections == 0 || config.sqlite.max_connections == 0 { + if !config.backend_options.is_object() { return std::result::Result::Err(ks_core::Error::new( - "database_max_connections_zero", - "database connection limits must be greater than zero", + "database_backend_options_invalid", + "database.backend_options must be a JSON object", )); } - if config.postgres.connect_timeout_ms == 0 || config.sqlite.busy_timeout_ms == 0 { - return std::result::Result::Err(ks_core::Error::new( - "database_timeout_zero", - "database timeouts must be greater than zero", - )); - } - return require_non_empty(&config.sqlite.path, "database.sqlite.path"); + return std::result::Result::Ok(()); } -fn validate_solana(config: &SolanaConfig) -> ks_core::Result<()> { +fn validate_solana(config: &crate::SolanaConfig) -> ks_core::Result<()> { if config.http_endpoints.is_empty() { return std::result::Result::Err(ks_core::Error::new( "http_endpoints_empty", @@ -522,7 +478,7 @@ fn validate_solana(config: &SolanaConfig) -> ks_core::Result<()> { endpoint.name.clone(), )); } - match validate_http_endpoint(endpoint) { + match crate::validate_http_endpoint(endpoint) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -535,15 +491,15 @@ fn validate_solana(config: &SolanaConfig) -> ks_core::Result<()> { endpoint.name.clone(), )); } - match validate_ws_endpoint(endpoint) { + match crate::validate_ws_endpoint(endpoint) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return validate_listeners(&config.listeners); + return crate::validate_listeners(&config.listeners); } -pub(crate) fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Result<()> { +pub(crate) fn validate_http_endpoint(config: &crate::HttpEndpointConfig) -> ks_core::Result<()> { match validate_endpoint_common(&config.name, &config.provider, &config.cluster, &config.roles) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -574,7 +530,7 @@ pub(crate) fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Re return std::result::Result::Ok(()); } -pub(crate) fn validate_ws_endpoint(config: &WsEndpointConfig) -> ks_core::Result<()> { +pub(crate) fn validate_ws_endpoint(config: &crate::WsEndpointConfig) -> ks_core::Result<()> { match validate_endpoint_common(&config.name, &config.provider, &config.cluster, &config.roles) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -603,17 +559,17 @@ fn validate_endpoint_common( name: &str, provider: &str, cluster: &str, - roles: &[EndpointRoleConfig], + roles: &[crate::EndpointRoleConfig], ) -> ks_core::Result<()> { - match require_non_empty(name, "endpoint.name") { + match crate::require_non_empty(name, "endpoint.name") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(provider, "endpoint.provider") { + match crate::require_non_empty(provider, "endpoint.provider") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(cluster, "endpoint.cluster") { + match crate::require_non_empty(cluster, "endpoint.cluster") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -639,8 +595,8 @@ fn validate_endpoint_common( return std::result::Result::Ok(()); } -fn validate_endpoint_role(config: &EndpointRoleConfig) -> ks_core::Result<()> { - match require_non_empty(&config.role, "endpoint.roles.role") { +fn validate_endpoint_role(config: &crate::EndpointRoleConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.role, "endpoint.roles.role") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -651,7 +607,7 @@ fn validate_endpoint_role(config: &EndpointRoleConfig) -> ks_core::Result<()> { )); } for request_kind in &config.request_kinds { - match require_non_empty(request_kind, "endpoint.roles.request_kinds") { + match crate::require_non_empty(request_kind, "endpoint.roles.request_kinds") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -669,8 +625,11 @@ fn validate_endpoint_role(config: &EndpointRoleConfig) -> ks_core::Result<()> { return std::result::Result::Ok(()); } -pub(crate) fn validate_listeners(config: &ListenerConfig) -> ks_core::Result<()> { - match require_non_empty(&config.default_commitment, "solana.listeners.default_commitment") { +pub(crate) fn validate_listeners(config: &crate::ListenerConfig) -> ks_core::Result<()> { + match crate::require_non_empty( + &config.default_commitment, + "solana.listeners.default_commitment", + ) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -695,62 +654,65 @@ pub(crate) fn validate_listeners(config: &ListenerConfig) -> ks_core::Result<()> return std::result::Result::Ok(()); } -fn validate_log_listener(config: &LogListenerConfig) -> ks_core::Result<()> { - match require_non_empty(&config.name, "log_listener.name") { +fn validate_log_listener(config: &crate::LogListenerConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.name, "log_listener.name") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(&config.endpoint_role, "log_listener.endpoint_role") { + match crate::require_non_empty(&config.endpoint_role, "log_listener.endpoint_role") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - return require_non_empty(&config.program_id, "log_listener.program_id"); + return crate::require_non_empty(&config.program_id, "log_listener.program_id"); } -fn validate_program_listener(config: &ProgramListenerConfig) -> ks_core::Result<()> { - match require_non_empty(&config.name, "program_listener.name") { +fn validate_program_listener(config: &crate::ProgramListenerConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.name, "program_listener.name") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(&config.endpoint_role, "program_listener.endpoint_role") { + match crate::require_non_empty(&config.endpoint_role, "program_listener.endpoint_role") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - return require_non_empty(&config.program_id, "program_listener.program_id"); + return crate::require_non_empty(&config.program_id, "program_listener.program_id"); } -fn validate_account_listener(config: &AccountListenerConfig) -> ks_core::Result<()> { - match require_non_empty(&config.name, "account_listener.name") { +fn validate_account_listener(config: &crate::AccountListenerConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.name, "account_listener.name") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(&config.endpoint_role, "account_listener.endpoint_role") { + match crate::require_non_empty(&config.endpoint_role, "account_listener.endpoint_role") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - return require_non_empty(&config.account_pubkey, "account_listener.account_pubkey"); + return crate::require_non_empty(&config.account_pubkey, "account_listener.account_pubkey"); } -pub(crate) fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> { - match require_non_empty(&config.wallet_dir, "wallet.wallet_dir") { +pub(crate) fn validate_wallet(config: &crate::WalletConfig) -> ks_core::Result<()> { + match crate::require_non_empty(&config.wallet_dir, "wallet.wallet_dir") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(&config.temporary_wallet_dir, "wallet.temporary_wallet_dir") { + match crate::require_non_empty(&config.temporary_wallet_dir, "wallet.temporary_wallet_dir") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - match require_non_empty(&config.cluster, "wallet.cluster") { + match crate::require_non_empty(&config.cluster, "wallet.cluster") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } if let std::option::Option::Some(alias) = config.wallet_alias.as_deref() { - match validate_wallet_alias(alias, "wallet.wallet_alias") { + match crate::validate_wallet_alias(alias, "wallet.wallet_alias") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } } - match validate_wallet_alias(&config.temporary_wallet_alias, "wallet.temporary_wallet_alias") { + match crate::validate_wallet_alias( + &config.temporary_wallet_alias, + "wallet.temporary_wallet_alias", + ) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } @@ -787,7 +749,7 @@ pub(crate) fn validate_wallet_alias(value: &str, field: &str) -> ks_core::Result return std::result::Result::Ok(()); } -pub(crate) fn validate_execution(config: &ExecutionConfig) -> ks_core::Result<()> { +pub(crate) fn validate_execution(config: &crate::ExecutionConfig) -> ks_core::Result<()> { let configured_spend = config .localnet_max_spend_lamports .saturating_add(config.devnet_max_spend_lamports) @@ -833,8 +795,8 @@ pub(crate) fn validate_execution(config: &ExecutionConfig) -> ks_core::Result<() } fn validate_wallet_execution_pair( - wallet: &WalletConfig, - execution: &ExecutionConfig, + wallet: &crate::WalletConfig, + execution: &crate::ExecutionConfig, ) -> ks_core::Result<()> { for (enabled, limit, cluster) in [ ( @@ -909,25 +871,25 @@ mod tests { #[test] fn schema_text_is_valid_json() { - let result = super::config_json_schema_value(); + let result = crate::config_json_schema_value(); assert!(result.is_ok()); } #[test] fn default_and_example_app_configs_validate_against_schema() { - assert!(super::validate_config_json_schema(DEFAULT_CONFIG).is_ok()); - assert!(super::validate_config_json_schema(EXAMPLE_CONFIG).is_ok()); + assert!(crate::validate_config_json_schema(DEFAULT_CONFIG).is_ok()); + assert!(crate::validate_config_json_schema(EXAMPLE_CONFIG).is_ok()); } #[test] fn default_app_config_parses_and_resolves_active_profile() { - let config_result = super::parse_config_json(DEFAULT_CONFIG); + let config_result = crate::parse_config_json(DEFAULT_CONFIG); assert!(config_result.is_ok()); let config = match config_result { std::result::Result::Ok(config) => config, std::result::Result::Err(error) => panic!("default app config must parse: {error}"), }; - let active_result = super::active_profile(&config); + let active_result = crate::active_profile(&config); assert!(active_result.is_ok()); //let active = match active_result { @@ -942,33 +904,62 @@ mod tests { let mut value = parse_default_value(); value["active_profile"] = serde_json::Value::String("missing_profile".to_string()); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } #[test] - fn default_app_config_separates_runtime_postgres_profiles() { + fn default_app_config_separates_selected_store_backend_options() { let value = parse_default_value(); assert_eq!( - value["profiles"][0]["database"]["postgres"]["url"], + value["profiles"][0]["database"]["backend_options"]["url"], serde_json::Value::String("${KS_SECRET_POSTGRES_DEVNET_URL}".to_string()) ); assert_eq!( - value["profiles"][1]["database"]["postgres"]["url"], + value["profiles"][1]["database"]["backend_options"]["url"], serde_json::Value::String("${KS_SECRET_POSTGRES_MAINNET_URL}".to_string()) ); assert_eq!( - value["profiles"][2]["database"]["postgres"]["url"], + value["profiles"][2]["database"]["backend_options"]["url"], serde_json::Value::String("${KS_SECRET_POSTGRES_MAINNET_URL}".to_string()) ); } + #[test] + fn parser_keeps_unknown_store_backend_options_opaque() { + let mut value = parse_default_value(); + value["profiles"][0]["database"]["backend"] = + serde_json::Value::String("future-backend".to_string()); + value["profiles"][0]["database"]["backend_options"] = + serde_json::json!({"secret": "STORE-SECRET-CANARY"}); + let raw_json = value_to_json(&value); + let config = match crate::parse_config_json(&raw_json) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("opaque backend must parse: {error}"), + }; + assert_eq!(config.profiles[0].database.backend, "future-backend"); + assert_eq!( + config.profiles[0].database.backend_options["secret"], + serde_json::Value::String("STORE-SECRET-CANARY".to_string()) + ); + } + + #[test] + fn schema_rejects_non_object_store_backend_options() { + let mut value = parse_default_value(); + value["profiles"][0]["database"]["backend_options"] = + serde_json::Value::String("invalid".to_string()); + let raw_json = value_to_json(&value); + let result = crate::validate_config_json_schema(&raw_json); + assert!(result.is_err()); + } + #[test] fn schema_rejects_profile_enabled_field() { let mut value = parse_default_value(); value["profiles"][0]["enabled"] = serde_json::Value::Bool(true); let raw_json = value_to_json(&value); - let result = super::validate_config_json_schema(&raw_json); + let result = crate::validate_config_json_schema(&raw_json); assert!(result.is_err()); } @@ -977,7 +968,7 @@ mod tests { let mut value = parse_default_value(); value["profiles"][1]["name"] = serde_json::Value::String("local_devnet".to_string()); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -987,7 +978,7 @@ mod tests { value["profiles"][0]["solana"]["http_endpoints"][0]["secret_env_field"] = serde_json::Value::String("KS_SECRET_HELIUS_API_KEY".to_string()); let raw_json = value_to_json(&value); - let result = super::validate_config_json_schema(&raw_json); + let result = crate::validate_config_json_schema(&raw_json); assert!(result.is_err()); } @@ -997,7 +988,7 @@ mod tests { value["profiles"][0]["data"]["idls_directory"] = serde_json::Value::String("idls".to_string()); let raw_json = value_to_json(&value); - let result = super::validate_config_json_schema(&raw_json); + let result = crate::validate_config_json_schema(&raw_json); assert!(result.is_err()); } @@ -1006,7 +997,7 @@ mod tests { let mut value = parse_default_value(); value["profiles"][0]["solana"]["advanced_streams"] = serde_json::json!([]); let raw_json = value_to_json(&value); - let result = super::validate_config_json_schema(&raw_json); + let result = crate::validate_config_json_schema(&raw_json); assert!(result.is_err()); } @@ -1016,7 +1007,7 @@ mod tests { value["profiles"][0]["solana"]["http_endpoints"][0]["url"] = serde_json::Value::String("wss://api.devnet.solana.com".to_string()); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -1026,7 +1017,7 @@ mod tests { value["profiles"][0]["solana"]["ws_endpoints"][0]["url"] = serde_json::Value::String("https://api.devnet.solana.com".to_string()); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -1036,7 +1027,7 @@ mod tests { value["profiles"][0]["wallet"]["wallet_alias"] = serde_json::Value::String("../wallet".to_string()); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -1046,7 +1037,7 @@ mod tests { value["profiles"][0]["wallet"]["temporary_wallet_alias"] = serde_json::Value::String("../wallet".to_string()); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -1055,7 +1046,7 @@ mod tests { let mut value = parse_default_value(); value["profiles"][0]["wallet"]["temporary_wallet_enabled"] = serde_json::Value::Bool(false); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -1065,7 +1056,7 @@ mod tests { value["profiles"][0]["execution"]["devnet_max_spend_lamports"] = serde_json::Value::Number(serde_json::Number::from(0)); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } @@ -1081,7 +1072,7 @@ mod tests { value["profiles"][0]["execution"][field] = serde_json::Value::Number(serde_json::Number::from(0)); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } } @@ -1092,7 +1083,7 @@ mod tests { value["profiles"][0]["solana"]["http_endpoints"][0]["roles"][0]["max_subscriptions"] = serde_json::Value::Number(serde_json::Number::from(1)); let raw_json = value_to_json(&value); - let result = super::parse_config_json(&raw_json); + let result = crate::parse_config_json(&raw_json); assert!(result.is_err()); } } diff --git a/ks-config/src/store.rs b/ks-config/src/store.rs index 42e8e8c..b5c73c3 100644 --- a/ks-config/src/store.rs +++ b/ks-config/src/store.rs @@ -1,5 +1,5 @@ // file: ks-config/src/store.rs -// version: 2 +// version: 4 //! Independent store configuration document loading and validation. @@ -11,7 +11,7 @@ pub struct StoreConfigDocument { /// Default store profile used when a composition does not override it. pub default_profile: std::string::String, /// Named store profiles. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Named store profile. @@ -42,7 +42,7 @@ pub fn store_json_schema_value() -> ks_core::Result { /// Validates raw store JSON against the embedded schema. pub fn validate_store_json_schema(raw_json: &str) -> ks_core::Result<()> { - let schema = match store_json_schema_value() { + let schema = match crate::store_json_schema_value() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -74,12 +74,12 @@ pub fn validate_store_json_schema(raw_json: &str) -> ks_core::Result<()> { } /// Parses and validates one store configuration document. -pub fn parse_store_json(raw_json: &str) -> ks_core::Result { - match validate_store_json_schema(raw_json) { +pub fn parse_store_json(raw_json: &str) -> ks_core::Result { + match crate::validate_store_json_schema(raw_json) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - let document = match serde_json::from_str::(raw_json) { + let document = match serde_json::from_str::(raw_json) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -88,7 +88,7 @@ pub fn parse_store_json(raw_json: &str) -> ks_core::Result )); }, }; - return match validate_store_document(&document) { + return match crate::validate_store_document(&document) { std::result::Result::Ok(()) => std::result::Result::Ok(document), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -98,7 +98,7 @@ pub fn parse_store_json(raw_json: &str) -> ks_core::Result pub fn read_store_json_file_with_environment( path: &std::path::Path, workspace_root: &std::path::Path, -) -> ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -112,14 +112,14 @@ pub fn read_store_json_file_with_environment( )); }, }; - return parse_store_json(&crate::resolve_environment_placeholders(&raw_json)); + return crate::parse_store_json(&crate::resolve_environment_placeholders(&raw_json)); } /// Returns one named store profile. pub fn store_profile<'a>( - document: &'a StoreConfigDocument, + document: &'a crate::StoreConfigDocument, profile_name: &str, -) -> ks_core::Result<&'a StoreProfileConfig> { +) -> ks_core::Result<&'a crate::StoreProfileConfig> { for profile in &document.profiles { if profile.name == profile_name { return std::result::Result::Ok(profile); @@ -133,13 +133,13 @@ pub fn store_profile<'a>( /// Returns the default store profile. pub fn default_store_profile( - document: &StoreConfigDocument, -) -> ks_core::Result<&StoreProfileConfig> { - return store_profile(document, document.default_profile.as_str()); + document: &crate::StoreConfigDocument, +) -> ks_core::Result<&crate::StoreProfileConfig> { + return crate::store_profile(document, document.default_profile.as_str()); } /// Validates one typed store configuration document. -pub fn validate_store_document(document: &StoreConfigDocument) -> ks_core::Result<()> { +pub fn validate_store_document(document: &crate::StoreConfigDocument) -> ks_core::Result<()> { match crate::require_non_empty(&document.default_profile, "store.default_profile") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -167,7 +167,7 @@ pub fn validate_store_document(document: &StoreConfigDocument) -> ks_core::Resul std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return match default_store_profile(document) { + return match crate::default_store_profile(document) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -180,7 +180,7 @@ mod tests { #[test] fn default_and_example_store_configs_validate() { - assert!(super::parse_store_json(DEFAULT_STORE).is_ok()); - assert!(super::parse_store_json(EXAMPLE_STORE).is_ok()); + assert!(crate::parse_store_json(DEFAULT_STORE).is_ok()); + assert!(crate::parse_store_json(EXAMPLE_STORE).is_ok()); } } diff --git a/ks-config/src/transport.rs b/ks-config/src/transport.rs index c6b5615..4c38ba0 100644 --- a/ks-config/src/transport.rs +++ b/ks-config/src/transport.rs @@ -1,5 +1,5 @@ // file: ks-config/src/transport.rs -// version: 3 +// version: 5 //! Independent Solana transport configuration document loading and validation. @@ -12,9 +12,9 @@ pub struct TransportConfigDocument { /// Default transport profile used when a composition does not override it. pub default_profile: std::string::String, /// Named WebSocket endpoint defaults resolved before endpoint-specific overrides. - pub ws_endpoint_defaults: std::vec::Vec, + pub ws_endpoint_defaults: std::vec::Vec, /// Named transport profiles available in this document. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Default values shared by one class of WebSocket endpoints. @@ -70,7 +70,7 @@ pub struct WsEndpointSourceConfig { pub url: std::string::String, /// Optional endpoint-specific overrides. #[serde(default)] - pub overrides: WsEndpointOverridesConfig, + pub overrides: crate::WsEndpointOverridesConfig, /// Role-specific limits for this URL. pub roles: std::vec::Vec, } @@ -83,7 +83,7 @@ pub struct TransportProfileSourceConfig { /// HTTP JSON-RPC endpoints. pub http_endpoints: std::vec::Vec, /// Standard Solana WebSocket endpoints before shared defaults are resolved. - pub ws_endpoints: std::vec::Vec, + pub ws_endpoints: std::vec::Vec, } /// Fully resolved transport profile consumed by transport clients. @@ -116,7 +116,7 @@ pub fn transport_json_schema_value() -> ks_core::Result { /// Validates raw transport JSON against the embedded transport schema. pub fn validate_transport_json_schema(raw_json: &str) -> ks_core::Result<()> { - let schema = match transport_json_schema_value() { + let schema = match crate::transport_json_schema_value() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -148,12 +148,12 @@ pub fn validate_transport_json_schema(raw_json: &str) -> ks_core::Result<()> { } /// Parses and validates one independent transport configuration document. -pub fn parse_transport_json(raw_json: &str) -> ks_core::Result { - match validate_transport_json_schema(raw_json) { +pub fn parse_transport_json(raw_json: &str) -> ks_core::Result { + match crate::validate_transport_json_schema(raw_json) { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } - let document = match serde_json::from_str::(raw_json) { + let document = match serde_json::from_str::(raw_json) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -162,7 +162,7 @@ pub fn parse_transport_json(raw_json: &str) -> ks_core::Result std::result::Result::Ok(document), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -172,7 +172,7 @@ pub fn parse_transport_json(raw_json: &str) -> ks_core::Result ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -186,14 +186,14 @@ pub fn read_transport_json_file_with_environment( )); }, }; - return parse_transport_json(&crate::resolve_environment_placeholders(&raw_json)); + return crate::parse_transport_json(&crate::resolve_environment_placeholders(&raw_json)); } /// Resolves one named transport profile including WebSocket defaults and overrides. pub fn transport_profile( - document: &TransportConfigDocument, + document: &crate::TransportConfigDocument, profile_name: &str, -) -> ks_core::Result { +) -> ks_core::Result { let source = match source_transport_profile(document, profile_name) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -259,7 +259,7 @@ pub fn transport_profile( std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return std::result::Result::Ok(TransportProfileConfig { + return std::result::Result::Ok(crate::TransportProfileConfig { name: source.name.clone(), http_endpoints: source.http_endpoints.clone(), ws_endpoints, @@ -268,15 +268,15 @@ pub fn transport_profile( /// Returns the default resolved transport profile. pub fn default_transport_profile( - document: &TransportConfigDocument, -) -> ks_core::Result { - return transport_profile(document, document.default_profile.as_str()); + document: &crate::TransportConfigDocument, +) -> ks_core::Result { + return crate::transport_profile(document, document.default_profile.as_str()); } fn source_transport_profile<'a>( - document: &'a TransportConfigDocument, + document: &'a crate::TransportConfigDocument, profile_name: &str, -) -> ks_core::Result<&'a TransportProfileSourceConfig> { +) -> ks_core::Result<&'a crate::TransportProfileSourceConfig> { for profile in &document.profiles { if profile.name == profile_name { return std::result::Result::Ok(profile); @@ -289,9 +289,9 @@ fn source_transport_profile<'a>( } fn ws_endpoint_defaults<'a>( - document: &'a TransportConfigDocument, + document: &'a crate::TransportConfigDocument, defaults_name: &str, -) -> ks_core::Result<&'a WsEndpointDefaultsConfig> { +) -> ks_core::Result<&'a crate::WsEndpointDefaultsConfig> { for defaults in &document.ws_endpoint_defaults { if defaults.name == defaults_name { return std::result::Result::Ok(defaults); @@ -304,7 +304,9 @@ fn ws_endpoint_defaults<'a>( } /// Validates a typed transport configuration document. -pub fn validate_transport_document(document: &TransportConfigDocument) -> ks_core::Result<()> { +pub fn validate_transport_document( + document: &crate::TransportConfigDocument, +) -> ks_core::Result<()> { match crate::require_non_empty(&document.default_profile, "transport.default_profile") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -379,12 +381,12 @@ pub fn validate_transport_document(document: &TransportConfigDocument) -> ks_cor std::result::Result::Err(error) => return std::result::Result::Err(error), } } - match transport_profile(document, profile.name.as_str()) { + match crate::transport_profile(document, profile.name.as_str()) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return match default_transport_profile(document) { + return match crate::default_transport_profile(document) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -398,17 +400,17 @@ mod tests { #[test] fn default_and_example_transport_configs_validate() { - assert!(super::parse_transport_json(DEFAULT_TRANSPORT).is_ok()); - assert!(super::parse_transport_json(EXAMPLE_TRANSPORT).is_ok()); + assert!(crate::parse_transport_json(DEFAULT_TRANSPORT).is_ok()); + assert!(crate::parse_transport_json(EXAMPLE_TRANSPORT).is_ok()); } #[test] fn websocket_defaults_are_resolved_before_endpoint_overrides() { - let document = match super::parse_transport_json(DEFAULT_TRANSPORT) { + let document = match crate::parse_transport_json(DEFAULT_TRANSPORT) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("default transport must parse: {error}"), }; - let profile = match super::transport_profile(&document, "mainnet_research") { + let profile = match crate::transport_profile(&document, "mainnet_research") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("mainnet research transport must resolve: {error}") diff --git a/ks-config/src/wallet.rs b/ks-config/src/wallet.rs index df0cf2a..d9508c1 100644 --- a/ks-config/src/wallet.rs +++ b/ks-config/src/wallet.rs @@ -1,5 +1,5 @@ // file: ks-config/src/wallet.rs -// version: 6 +// version: 8 //! Independent wallet configuration document loading and validation. @@ -13,7 +13,7 @@ pub struct WalletConfigDocument { /// Default wallet profile used when a composition does not override it. pub default_profile: std::string::String, /// Named wallet profiles. - pub profiles: std::vec::Vec, + pub profiles: std::vec::Vec, } /// Named wallet profile whose directory is relative to the global wallet root unless absolute. @@ -41,7 +41,7 @@ pub fn wallet_json_schema_text() -> &'static str { } /// Parses and validates one wallet configuration document. -pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result { +pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result { let schema = match serde_json::from_str::(WALLET_JSON_SCHEMA) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { @@ -75,7 +75,7 @@ pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result(raw_json) { + let document = match serde_json::from_str::(raw_json) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(ks_core::Error::new( @@ -84,7 +84,7 @@ pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result std::result::Result::Ok(document), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -94,7 +94,7 @@ pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result ks_core::Result { +) -> ks_core::Result { match crate::load_workspace_environment(workspace_root) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -108,14 +108,14 @@ pub fn read_wallet_json_file_with_environment( )); }, }; - return parse_wallet_json(&crate::resolve_environment_placeholders(&raw_json)); + return crate::parse_wallet_json(&crate::resolve_environment_placeholders(&raw_json)); } /// Returns one named wallet source profile. pub fn wallet_profile<'a>( - document: &'a WalletConfigDocument, + document: &'a crate::WalletConfigDocument, profile_name: &str, -) -> ks_core::Result<&'a WalletProfileConfig> { +) -> ks_core::Result<&'a crate::WalletProfileConfig> { for profile in &document.profiles { if profile.name == profile_name { return std::result::Result::Ok(profile); @@ -129,17 +129,17 @@ pub fn wallet_profile<'a>( /// Returns the default wallet source profile. pub fn default_wallet_profile( - document: &WalletConfigDocument, -) -> ks_core::Result<&WalletProfileConfig> { - return wallet_profile(document, document.default_profile.as_str()); + document: &crate::WalletConfigDocument, +) -> ks_core::Result<&crate::WalletProfileConfig> { + return crate::wallet_profile(document, document.default_profile.as_str()); } /// Resolves one source wallet profile into the runtime wallet contract. pub fn resolved_wallet_profile( - document: &WalletConfigDocument, + document: &crate::WalletConfigDocument, profile_name: &str, ) -> ks_core::Result { - let profile = match wallet_profile(document, profile_name) { + let profile = match crate::wallet_profile(document, profile_name) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; @@ -166,7 +166,7 @@ pub fn resolved_wallet_profile( } /// Validates one typed wallet configuration document. -pub fn validate_wallet_document(document: &WalletConfigDocument) -> ks_core::Result<()> { +pub fn validate_wallet_document(document: &crate::WalletConfigDocument) -> ks_core::Result<()> { match crate::require_non_empty(&document.wallets_directory, "wallet.wallets_directory") { std::result::Result::Ok(()) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -203,12 +203,12 @@ pub fn validate_wallet_document(document: &WalletConfigDocument) -> ks_core::Res profile.name.clone(), )); } - match resolved_wallet_profile(document, profile.name.as_str()) { + match crate::resolved_wallet_profile(document, profile.name.as_str()) { std::result::Result::Ok(_) => (), std::result::Result::Err(error) => return std::result::Result::Err(error), } } - return match default_wallet_profile(document) { + return match crate::default_wallet_profile(document) { std::result::Result::Ok(_) => std::result::Result::Ok(()), std::result::Result::Err(error) => std::result::Result::Err(error), }; @@ -221,9 +221,9 @@ mod tests { #[test] fn default_and_example_wallet_configs_validate() { - let default = super::parse_wallet_json(DEFAULT_WALLET); + let default = crate::parse_wallet_json(DEFAULT_WALLET); assert!(default.is_ok()); - let example = super::parse_wallet_json(EXAMPLE_WALLET); + let example = crate::parse_wallet_json(EXAMPLE_WALLET); assert!(example.is_ok()); let example = match example { std::result::Result::Ok(value) => value, @@ -238,11 +238,11 @@ mod tests { #[test] fn resolved_profile_separates_persistent_and_temporary_wallet_directories() { let resolved_raw = crate::resolve_environment_placeholders(DEFAULT_WALLET); - let document = match super::parse_wallet_json(resolved_raw.as_str()) { + let document = match crate::parse_wallet_json(resolved_raw.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("resolved wallet config failed: {error}"), }; - let resolved = match super::resolved_wallet_profile(&document, "local_devnet") { + let resolved = match crate::resolved_wallet_profile(&document, "local_devnet") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("wallet profile resolution failed: {error}"), }; @@ -269,7 +269,7 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("wallet JSON encode failed: {error}"), }; - let parsed = super::parse_wallet_json(raw.as_str()); + let parsed = crate::parse_wallet_json(raw.as_str()); assert!(parsed.is_ok()); let parsed = match parsed { std::result::Result::Ok(value) => value, @@ -289,6 +289,6 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("wallet JSON encode failed: {error}"), }; - assert!(super::parse_wallet_json(raw.as_str()).is_err()); + assert!(crate::parse_wallet_json(raw.as_str()).is_err()); } } diff --git a/ks-pipeline-demo-scenarios/CHANGELOG.md b/ks-pipeline-demo-scenarios/CHANGELOG.md index 9bfbd38..4b0fa68 100644 --- a/ks-pipeline-demo-scenarios/CHANGELOG.md +++ b/ks-pipeline-demo-scenarios/CHANGELOG.md @@ -1,8 +1,14 @@ - + # CHANGELOG — ks-pipeline-demo-scenarios +## `0.5.3-pre.002` + +- remplace les constructions `PostgresStore`/`PostgresStoreOptions` des scénarios par `ks_store::Store` et `StoreOpenOptions` ; +- conserve les overrides PostgreSQL des tests opt-in sous forme de modification du bloc opaque `backend_options`, sans exposer de type backend dans les signatures de scénarios ; +- adapte les campagnes Solana/SPL/Metadata afin qu'elles réutilisent la même façade de store que `ks-pipeline` et le desktop. + ## `0.5.2-pre.006-delta-fix-006` - corrige le wrapper pNFT legacy : il reprend son paramètre `workspace_root` historique et construit le wallet temporaire uniquement dans ce wrapper, tandis que la variante `_with_wallet` conserve le signer explicite ; diff --git a/ks-pipeline-demo-scenarios/README.md b/ks-pipeline-demo-scenarios/README.md index a5949fe..20c3ac8 100644 --- a/ks-pipeline-demo-scenarios/README.md +++ b/ks-pipeline-demo-scenarios/README.md @@ -1,10 +1,12 @@ - + # ks-pipeline-demo-scenarios `ks-pipeline-demo-scenarios` fournit des scénarios synthétiques et des campagnes Devnet/Testnet automatisables au-dessus de `ks-pipeline`. +La crate ouvre désormais le stockage de ses profils via la façade backend-agnostique `ks_store::Store`. Elle ne construit ni `PostgresStore`, ni pool SQLx ; les paramètres du moteur restent interprétés par `ks-store`. + La crate contient : - la bibliothèque Rust `ks_pipeline_demo_scenarios` ; diff --git a/ks-pipeline-demo-scenarios/USAGE.md b/ks-pipeline-demo-scenarios/USAGE.md index 6d1e8e9..27983a9 100644 --- a/ks-pipeline-demo-scenarios/USAGE.md +++ b/ks-pipeline-demo-scenarios/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ks-pipeline-demo-scenarios @@ -21,7 +21,7 @@ fn initialize_environment() -> ks_core::Result<()> { } ``` -`resolve_demo_devnet_profile` sélectionne un profil Devnet configuré et `prepare_demo_devnet_profile_store` vérifie ou initialise son stockage PostgreSQL. +`resolve_demo_devnet_profile` sélectionne un profil Devnet configuré et `prepare_demo_devnet_profile_store` ouvre/vérifie son `ks_store::Store`. Le scénario ne connaît pas le backend concret ; PostgreSQL reste actuellement sélectionné par la configuration mais sa connexion appartient à `ks-store`. ## Observer une exécution @@ -63,13 +63,12 @@ fn memo_request() -> ks_pipeline_demo_scenarios::DevnetMemoExecutionRequest { ``` ```rust -async fn run_memo( - store: &S, +async fn run_memo( + store: &ks_store::Store, observer: &O, request: ks_pipeline_demo_scenarios::DevnetMemoExecutionRequest, ) -> ks_core::Result where - S: ks_store::Store + Sync, O: ks_pipeline_demo_scenarios::SolanaExecutionObserver, { let result = ks_pipeline_demo_scenarios::execute_devnet_memo( @@ -78,14 +77,12 @@ where request, ) .await; - match result { Ok(summary) => Ok(summary), Err(error) => Err(error), } } ``` - ## Simuler Token-2022 ```rust diff --git a/ks-pipeline-demo-scenarios/src/environment.rs b/ks-pipeline-demo-scenarios/src/environment.rs index c5a9e26..2fd362b 100644 --- a/ks-pipeline-demo-scenarios/src/environment.rs +++ b/ks-pipeline-demo-scenarios/src/environment.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/environment.rs -// version: 7 +// version: 9 //! Environment initialization for opt-in demonstration scenarios. @@ -83,28 +83,53 @@ pub fn resolve_demo_devnet_profile( }; } -/// Readiness report for one Devnet profile PostgreSQL store. +/// Backend-agnostic readiness report for one Devnet profile store. #[derive(Clone, Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct DevnetProfileStoreReadiness { /// Selected Devnet profile. pub profile_name: std::string::String, + /// Selected backend code. + pub backend: std::string::String, /// Whether schema creation was allowed by configuration. pub auto_initialize_schema: bool, - /// Number of known tables already present before preparation. - pub existing_tables_before: usize, - /// Number of known tables created during preparation. - pub created_tables: usize, - /// Number of known tables present after preparation. - pub existing_tables_after: usize, - /// Total number of known tables expected by the current store. - pub expected_tables: usize, + /// Number of known logical resources already present before preparation. + pub available_resources_before: usize, + /// Number of known logical resources created during preparation. + pub created_resources: usize, + /// Number of known logical resources present after preparation. + pub available_resources_after: usize, + /// Total number of logical resources expected by the current store contract. + pub expected_resources: usize, } -/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile. +/// Builds backend-agnostic store-open options from one resolved profile. +fn store_open_options_from_profile( + profile: &ks_config::ProfileConfig, +) -> ks_core::Result { + return ks_store::StoreOpenOptions::new( + profile.database.enabled, + profile.database.backend.clone(), + profile.database.backend_options.clone(), + ); +} + +/// Opens the backend-agnostic store selected by one resolved profile. +#[cfg(test)] +pub(crate) async fn open_profile_store( + profile: &ks_config::ProfileConfig, +) -> ks_core::Result { + let options = match store_open_options_from_profile(profile) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return ks_store::Store::open(options).await; +} + +/// Verifies or initializes the store selected by one Devnet profile. pub async fn prepare_demo_devnet_profile_store( profile: &ks_config::ProfileConfig, -) -> ks_core::Result { +) -> ks_core::Result { if profile.wallet.cluster != "devnet" { return std::result::Result::Err(ks_core::Error::config(format!( "profile '{}' is not configured for Devnet", @@ -117,65 +142,78 @@ pub async fn prepare_demo_devnet_profile_store( profile.name ))); } - if profile.database.backend != "postgres" { + let options = match store_open_options_from_profile(profile) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let configuration = match options.configuration_summary() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let store = match ks_store::Store::open(options).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let initialization = store.initialization_summary(); + if initialization.status != ks_store::StoreInitializationStatus::Ready { return std::result::Result::Err(ks_core::Error::config(format!( - "Devnet profile '{}' must use the PostgreSQL backend", - profile.name + "Devnet profile '{}' store model is incomplete and automatic initialization is {}", + profile.name, + if configuration.auto_initialize_schema { "enabled" } else { "disabled" } ))); } - let options = match ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, + return std::result::Result::Ok(crate::DevnetProfileStoreReadiness { + profile_name: profile.name.clone(), + backend: configuration.backend_code, + auto_initialize_schema: configuration.auto_initialize_schema, + available_resources_before: initialization + .available_resource_count + .saturating_sub(initialization.created_resource_count) + as usize, + created_resources: initialization.created_resource_count as usize, + available_resources_after: initialization.available_resource_count as usize, + expected_resources: initialization.expected_resource_count as usize, + }); +} + +#[cfg(test)] +pub(crate) fn override_profile_store_url_for_test( + profile: &mut ks_config::ProfileConfig, + database_url: std::string::String, +) -> ks_core::Result<()> { + profile.database.enabled = true; + profile.database.backend = "postgres".to_string(); + let backend_options = match profile.database.backend_options.as_object_mut() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(ks_core::Error::config( + "test store backend options must be an object", + )); + }, + }; + backend_options.insert("url".to_string(), serde_json::Value::String(database_url)); + backend_options.insert("auto_initialize_schema".to_string(), serde_json::Value::Bool(true)); + return std::result::Result::Ok(()); +} + +#[cfg(test)] +pub(crate) async fn open_test_store( + database_url: std::string::String, +) -> ks_core::Result { + let options = match ks_store::StoreOpenOptions::new( + true, + "postgres", + serde_json::json!({ + "url": database_url, + "max_connections": 5, + "connect_timeout_ms": 5_000, + "auto_initialize_schema": true + }), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let store = match ks_store::PostgresStore::connect(options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let before = match store.known_table_diagnostics().await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let existing_tables_before = before.iter().filter(|table| return table.exists).count(); - if profile.database.postgres.auto_initialize_schema - && let std::result::Result::Err(error) = store.initialize_store_schema().await - { - return std::result::Result::Err(error); - } - let after = match store.known_table_diagnostics().await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return std::result::Result::Err(error), - }; - let existing_tables_after = after.iter().filter(|table| return table.exists).count(); - if existing_tables_after != after.len() { - let missing = after - .iter() - .filter(|table| return !table.exists) - .map(|table| return table.table_name.clone()) - .collect::>(); - return std::result::Result::Err(ks_core::Error::config(format!( - "Devnet profile '{}' PostgreSQL schema is incomplete and auto initialization is {}: missing {}", - profile.name, - if profile.database.postgres.auto_initialize_schema { - "enabled" - } else { - "disabled" - }, - missing.join(", ") - ))); - } - return std::result::Result::Ok(DevnetProfileStoreReadiness { - profile_name: profile.name.clone(), - auto_initialize_schema: profile.database.postgres.auto_initialize_schema, - existing_tables_before, - created_tables: existing_tables_after.saturating_sub(existing_tables_before), - existing_tables_after, - expected_tables: after.len(), - }); + return ks_store::Store::open(options).await; } #[cfg(test)] diff --git a/ks-pipeline-demo-scenarios/src/lib.rs b/ks-pipeline-demo-scenarios/src/lib.rs index 80d0834..0b7ecf1 100644 --- a/ks-pipeline-demo-scenarios/src/lib.rs +++ b/ks-pipeline-demo-scenarios/src/lib.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/lib.rs -// version: 31 +// version: 34 #![forbid(unsafe_code)] #![deny(unreachable_pub)] @@ -14,17 +14,27 @@ mod solana; mod spl; mod wallet; -/// Readiness report for one Devnet profile PostgreSQL store. +/// Backend-agnostic readiness report for one Devnet profile store. pub use self::environment::DevnetProfileStoreReadiness; /// Loads the workspace environment for opt-in demo and Devnet scenarios. pub use self::environment::initialize_demo_scenario_environment; /// Loads the shared default configuration or an explicit scenario composition override. pub use self::environment::load_demo_scenario_config; -/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile. +/// Verifies or initializes the store selected by one Devnet profile. pub use self::environment::prepare_demo_devnet_profile_store; /// Resolves one configured Devnet profile for UI, CLI and test scenarios. pub use self::environment::resolve_demo_devnet_profile; -/// Complete evidence produced by one collection parent/member verification campaign. + +#[cfg(test)] +/// Opens one backend-agnostic store for internal scenarios. +pub(crate) use self::environment::open_profile_store; +#[cfg(test)] +/// Opens one backend-agnostic store from a direct test URL. +pub(crate) use self::environment::open_test_store; +#[cfg(test)] +/// Overrides one profile store URL for PostgreSQL integration tests without exposing backend types. +pub(crate) use self::environment::override_profile_store_url_for_test; +/// Complete evidence retained by one Metaplex collection verification campaign. pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::DevnetMetaplexCollectionVerifyCampaignSummary; /// Collection relation and sized-collection state observed across verification. pub use self::metadata::metaplex_token_metadata::collection_verify_campaign::DevnetMetaplexCollectionVerifyState; @@ -88,7 +98,7 @@ pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_token_account_state; /// Reads the validated classic SPL token-account amount for an internal Metaplex campaign. pub(crate) use self::metadata::metaplex_token_metadata::fixture::classic_fixture_token_amount; -/// Creates or reuses a classic SPL mint and derives its Metaplex PDAs. +/// Creates the default classic SPL fixture required by one Metaplex Create campaign. pub use self::metadata::metaplex_token_metadata::fixture::prepare_metaplex_create_fixture; /// Creates a classic SPL mint fixture using an explicitly resolved operator wallet. pub use self::metadata::metaplex_token_metadata::fixture::prepare_metaplex_create_fixture_with_wallet; @@ -100,7 +110,7 @@ pub(crate) use self::metadata::metaplex_token_metadata::fixture::read_token_acco pub(crate) use self::metadata::metaplex_token_metadata::fixture::validate_classic_fixture_mint_state; /// Validates one classic SPL token-account fixture against an exact expected amount. pub(crate) use self::metadata::metaplex_token_metadata::fixture::validate_classic_fixture_token_account_state; -/// Complete evidence produced by the final Metaplex maintenance campaign. +/// Complete evidence retained by one bounded Metaplex maintenance campaign. pub use self::metadata::metaplex_token_metadata::maintenance_campaign::DevnetMetaplexMaintenanceCampaignSummary; /// Executes the final bounded current maintenance campaign on Devnet. pub use self::metadata::metaplex_token_metadata::maintenance_campaign::execute_devnet_metaplex_maintenance_campaign; @@ -256,7 +266,7 @@ pub use self::solana::SolanaExecutionObserver; pub use self::solana::SolanaExecutionProgressEvent; /// Severity of one Solana execution progress event. pub use self::solana::SolanaExecutionProgressLevel; -/// Emits one execution progress event for specialized pipeline orchestrators. +/// Emits one execution progress event through the optional internal observer. pub(crate) use self::solana::emit; /// Rejects one execution stage when the observer reports cancellation. pub(crate) use self::solana::ensure_not_cancelled; @@ -266,7 +276,7 @@ pub use self::solana::execute_devnet_system_transfer; pub use self::solana::execute_devnet_system_transfer_with_wallet; /// Inspects the persistent native wallet alias selected by one execution profile. pub use self::solana::inspect_selected_profile_wallet; -/// Loads the managed temporary wallet used by legacy Devnet scenarios. +/// Loads the temporary wallet fixture selected by one execution profile. pub(crate) use self::solana::load_profile_temporary_wallet; /// Formats one failed simulation without discarding runtime diagnostics. pub(crate) use self::solana::simulation_failure_message; @@ -296,7 +306,7 @@ pub use self::spl::memo::execute_devnet_memo_with_wallet; pub use self::spl::token::execution::DevnetSplTokenExecutionRequest; /// Complete result of one Devnet classic SPL Token execution. pub use self::spl::token::execution::DevnetSplTokenExecutionSummary; -/// Canonical hydration availability helper shared by Token lifecycle orchestration. +/// Reports whether canonical transaction data is available for one signature. pub(crate) use self::spl::token::execution::canonical_available; /// Decode completion helper shared by Token lifecycle orchestration. pub(crate) use self::spl::token::execution::decode_completed; @@ -304,7 +314,7 @@ pub(crate) use self::spl::token::execution::decode_completed; pub use self::spl::token::execution::execute_devnet_spl_token; /// Executes one SPL Token operation with an explicitly resolved wallet capability. pub use self::spl::token::execution::execute_devnet_spl_token_with_wallet; -/// Canonical signature hydration helper shared by Token lifecycle orchestration. +/// Hydrates one submitted signature into the canonical raw/Core pipeline. pub(crate) use self::spl::token::execution::hydrate_signature; /// Targeted program replay helper shared by Token lifecycle orchestration. pub(crate) use self::spl::token::execution::replay_program; diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs index dbe86a7..069ba31 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs -// version: 7 +// version: 9 //! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign. @@ -678,23 +678,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs index 70b52e2..a558431 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs -// version: 15 +// version: 17 //! Confirmed Devnet `Create -> Mint` campaign for one Metaplex asset family. @@ -844,23 +844,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs index b003d9b..03cc78d 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs -// version: 24 +// version: 26 //! Real Devnet simulation and submission for current Metaplex Token Metadata operations. @@ -1516,18 +1516,10 @@ mod tests { panic!("KS_SECRET_POSTGRES_TEST_URL is required for Metaplex execution: {error}") }, }; - let store_options = match ks_store::PostgresStoreOptions::new(database_url, 5, 5_000, false) - { + let store = match crate::open_test_store(database_url).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("Postgres options failed: {error}"), + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), }; - let store = match ks_store::PostgresStore::connect(store_options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("Postgres connection failed: {error}"), - }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("Postgres schema initialization failed: {error}"); - } let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() { std::option::Option::Some(value) => value, std::option::Option::None => panic!("workspace root cannot be resolved"), diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs index 9bebc42..42a62f4 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs -// version: 7 +// version: 9 //! Confirmed Devnet Token Owned Escrow campaign for current Metaplex escrow operations. @@ -1004,23 +1004,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs index 13e7438..46c689e 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs -// version: 9 +// version: 11 //! Final bounded Devnet qualification campaign for current Metaplex maintenance operations. @@ -598,23 +598,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs index 8df609b..c2876c4 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs -// version: 10 +// version: 12 //! Confirmed Devnet pNFT delegate, lock, unlock, revoke and transfer campaign. @@ -1096,23 +1096,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs index bcb2c05..2a73e2e 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs -// version: 10 +// version: 12 //! Devnet printable master NFT `Print -> Burn` campaign. @@ -1210,23 +1210,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs index e23b615..73a172d 100644 --- a/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs +++ b/ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs -// version: 7 +// version: 9 //! Bounded Devnet availability probe for the current Metaplex `Use` surface. @@ -455,23 +455,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let http_pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool initialization failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-pipeline-demo-scenarios/src/solana.rs b/ks-pipeline-demo-scenarios/src/solana.rs index 8c9c2c1..f801e65 100644 --- a/ks-pipeline-demo-scenarios/src/solana.rs +++ b/ks-pipeline-demo-scenarios/src/solana.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/solana.rs -// version: 18 +// version: 20 //! Devnet Solana execution orchestration with canonical post-validation. @@ -1544,8 +1544,8 @@ mod tests { }, }; let mut profile = example_devnet_profile(); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); if let std::result::Result::Ok(directory) = std::env::var("KS_DEVNET_WALLET_DIR") { profile.wallet.temporary_wallet_dir = directory; } @@ -1553,22 +1553,10 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"), }; - let store_options = match ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) { + let store = match crate::open_profile_store(&profile).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL options failed: {error}"), + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), }; - let store = match ks_store::PostgresStore::connect(store_options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"), - }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } let recipient_alias = match ks_wallet::WalletAlias::parse("devnet-recipient") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("recipient alias failed: {error}"), diff --git a/ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs b/ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs index 2d98a4d..9a172cd 100644 --- a/ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs +++ b/ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs -// version: 17 +// version: 19 //! Devnet ATA execution with stateful and canonical post-validation. @@ -1162,20 +1162,10 @@ mod tests { request.post_validation_max_retries = 20; let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - store - .initialize_store_schema() - .await - .unwrap_or_else(|error| panic!("schema initialization failed: {error}")); + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let decoders: std::vec::Vec> = std::vec![ std::sync::Arc::new(ks_lib::DcSplAssociatedTokenAccountDecoder), std::sync::Arc::new(ks_lib::DcSplTokenDecoder), diff --git a/ks-pipeline-demo-scenarios/src/spl/memo.rs b/ks-pipeline-demo-scenarios/src/spl/memo.rs index fb57481..e93c06c 100644 --- a/ks-pipeline-demo-scenarios/src/spl/memo.rs +++ b/ks-pipeline-demo-scenarios/src/spl/memo.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/spl/memo.rs -// version: 13 +// version: 15 //! Devnet SPL Memo v4 execution with canonical post-validation. @@ -857,8 +857,8 @@ mod tests { }, }; let mut profile = example_devnet_profile(); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); if let std::result::Result::Ok(directory) = std::env::var("KS_DEVNET_WALLET_DIR") { profile.wallet.temporary_wallet_dir = directory; } @@ -866,22 +866,10 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"), }; - let store_options = match ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) { + let store = match crate::open_profile_store(&profile).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL options failed: {error}"), + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), }; - let store = match ks_store::PostgresStore::connect(store_options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"), - }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } let mut request = crate::DevnetMemoExecutionRequest::new( format!("devnet-memo-test-{}", uuid::Uuid::new_v4()), format!("khadhroony-bot3 memo validation {}", uuid::Uuid::new_v4()), diff --git a/ks-pipeline-demo-scenarios/src/spl/token/execution.rs b/ks-pipeline-demo-scenarios/src/spl/token/execution.rs index 6860d8c..e1617f7 100644 --- a/ks-pipeline-demo-scenarios/src/spl/token/execution.rs +++ b/ks-pipeline-demo-scenarios/src/spl/token/execution.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/spl/token/execution.rs -// version: 17 +// version: 19 //! Devnet classic SPL Token execution with stateful and canonical post-validation. @@ -1189,24 +1189,12 @@ mod tests { panic!("KS_SECRET_POSTGRES_TEST_URL is required for submission: {error}"); }, }; - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; - let store_options = match ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) { + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); + let store = match crate::open_profile_store(&profile).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL options failed: {error}"), + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), }; - let store = match ks_store::PostgresStore::connect(store_options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"), - }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } let decoders: std::vec::Vec> = std::vec![std::sync::Arc::new(ks_lib::DcSplTokenDecoder)]; let materializers: std::vec::Vec> = diff --git a/ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs b/ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs index 05980e4..5cd16c5 100644 --- a/ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs +++ b/ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs -// version: 16 +// version: 18 //! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts. @@ -1520,28 +1520,16 @@ mod tests { panic!("KS_SECRET_POSTGRES_TEST_URL is required for lifecycle submission: {error}"); }, }; - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(&profile) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"), }; - let store_options = match ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) { + let store = match crate::open_profile_store(&profile).await { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL options failed: {error}"), + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), }; - let store = match ks_store::PostgresStore::connect(store_options).await { - std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"), - }; - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() { std::option::Option::Some(value) => value, std::option::Option::None => panic!("workspace root cannot be resolved"), diff --git a/ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs b/ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs index 0f995d9..a36e5a1 100644 --- a/ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs +++ b/ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs @@ -1,5 +1,5 @@ // file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs -// version: 10 +// version: 12 //! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions. @@ -529,23 +529,14 @@ mod tests { .unwrap_or_else(|error| panic!("Devnet profile resolution failed: {error}")); let database_url = std::env::var("KS_SECRET_POSTGRES_TEST_URL") .unwrap_or_else(|error| panic!("KS_SECRET_POSTGRES_TEST_URL is required: {error}")); - profile.database.backend = "postgres".to_string(); - profile.database.postgres.url = database_url; + crate::override_profile_store_url_for_test(&mut profile, database_url) + .unwrap_or_else(|error| panic!("Store test override failed: {error}")); let pool = ks_onchain_transport::HttpEndpointPool::from_profile(&profile) .unwrap_or_else(|error| panic!("HTTP pool creation failed: {error}")); - let store_options = ks_store::PostgresStoreOptions::new( - profile.database.postgres.url.clone(), - profile.database.postgres.max_connections, - profile.database.postgres.connect_timeout_ms, - false, - ) - .unwrap_or_else(|error| panic!("PostgreSQL options failed: {error}")); - let store = ks_store::PostgresStore::connect(store_options) - .await - .unwrap_or_else(|error| panic!("PostgreSQL connection failed: {error}")); - if let std::result::Result::Err(error) = store.initialize_store_schema().await { - panic!("PostgreSQL schema initialization failed: {error}"); - } + let store = match crate::open_profile_store(&profile).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("Store connection failed: {error}"), + }; let configured_wallet_dir = std::path::PathBuf::from(profile.wallet.temporary_wallet_dir.as_str()); let wallet_dir = if configured_wallet_dir.is_absolute() { diff --git a/ks-store/CHANGELOG.md b/ks-store/CHANGELOG.md index 4b9943a..80bcb2e 100644 --- a/ks-store/CHANGELOG.md +++ b/ks-store/CHANGELOG.md @@ -1,8 +1,48 @@ - + # CHANGELOG — ks-store +## `0.5.3-pre.002-delta-fix-003` + +- corrige deux appels de helpers strictement privés PostgreSQL qui avaient été qualifiés à tort via `crate::`; ils restent locaux à leur module et ne sont pas réexportés au crate-root ; +- ne modifie aucun contrat public ni schéma SQL. + +## `0.5.3-pre.002-delta-fix-002` + +- remplace les visibilités `pub(in crate::postgres)` par la convention workspace : privé ou `pub(crate)` + réexport crate-root et consommation via `crate::Item` ; +- ajoute le canari Rust interdisant `pub(in ...)`/`pub(super)` et corrige les reexports `#[cfg(test)]` de `ks-pipeline-demo-scenarios` ; +- supprime les appels obsolètes à `Store::initialize_store_schema()` : l’initialisation reste possédée par `Store::open()` via la configuration backend ; +- renomme structurellement les modules, fenêtres, commandes, HTML/TypeScript et DTO `demo_sql_*` en `demo_store_*`, sans attendre la refonte fonctionnelle finale de `pre.005` ; +- réconcilie le tracing PostgreSQL avec la règle d’un target unique : `target: crate::TRACING_TARGET`, `backend="postgres"`, `domain="ks-store.pg"`, `action=...` ; +- supprime les helpers/constantes transitoires morts signalés par `cargo check`/Clippy et l’attente Clippy devenue inutile. + +## `0.5.3-pre.002-delta-fix-001` + +- corrige les helpers PostgreSQL de validation de noms de tables rendus inaccessibles par la nouvelle frontière privée et aligne leurs appels/tests sur la façade interne `crate::postgres::*` ; +- supprime les réexports PostgreSQL internes devenus inutiles qui produisaient des warnings et conserve le target générique `ks-store` en l'utilisant réellement à l'ouverture de la façade `Store` ; +- aligne les nouveaux types publics/crate-publics introduits par `pre.002` sur la règle de qualification `crate::Item` dans leur crate propriétaire. + +## `0.5.3-pre.002` + +### Façade et configuration + +- introduit `Store` et `StoreOpenOptions` comme frontière publique backend-agnostique et confine `PostgresStore`, `PostgresStoreOptions`, `PgPool`, les constantes physiques et les requêtes SQL à l'implémentation privée ; +- remplace les sections runtime PostgreSQL/SQLite imposées par un code `backend` et un objet `backend_options` opaque jusqu'à son interprétation par `ks-store`, sans créer de dépendance `ks-store -> ks-config` ; +- retire les quatre anciens traits/DTO historiques sans implémentation réelle et renomme les contrats publics de replay/diagnostic sans préfixe PostgreSQL ; +- conserve `Store` dans l'état applicatif desktop via une initialisation unique et réutilisable par les commandes/scénarios. + +### Diagnostics et sécurité + +- ajoute les résumés génériques de configuration, backend, initialisation et vérification par modèle, sans exposition des noms physiques de tables/index ni des options backend brutes ; +- masque le descripteur PostgreSQL et sanitise l'erreur de connexion afin qu'aucun DSN/password ne traverse les diagnostics normaux ; +- introduit le domaine structuré PostgreSQL `ks-store.pg` sous l’unique `target: crate::TRACING_TARGET`, ainsi qu'un canari workspace empêchant les types PostgreSQL/SQLx de sortir de `ks-store` ; +- adapte les consommateurs Rust et les payloads desktop aux ressources/diagnostics génériques ; le renommage structurel `demo_sql_* -> demo_store_*` est finalisé par `delta-fix-002`, tandis que la refonte fonctionnelle finale reste à la tranche consommateurs. + +### Portée + +- ne modifie aucune table ni migration SQL de production : le namespace `k_sol_*`, `block_time`, la symétrie top-level/CPI et les ressources SQL atomiques restent réservés à `pre.003`. + ## `0.5.3-pre.001` ### Audit / plan @@ -16,7 +56,7 @@ - normalise le vocabulaire d'instruction vers `top-level`, conserve les CPI séparées et exige `stack_height`/parent CPI immédiat lorsque reconstructible ; - ajoute un canari de gel future-decoder/Anchor-readiness : N1/N2/N3 doivent conserver les données Solana génériques nécessaires aux instructions/CPI, événements/logs, `returnData`, états/observations de comptes et provenance de schéma, sans ajouter de contrat `anchor_*` ; les 13 tables existantes restent une baseline et non un quota final si un manque générique est découvert avant gel ; - formalise la reconstruction `kb_sol_* -> k_sol_*`, l'arborescence `migrations/postgres/`, une instruction SQL par fichier exécutée via `include_str!`, l'audit des index/paginations/BIGINT et les règles de replay/invalidation ; -- définit la façade de stockage backend-agnostique, l'encapsulation de PostgreSQL, la stratégie de configuration backend opaque jusqu'à `ks-store`, le résumé runtime/init sans secrets, la future réutilisation d'un `Store` persistant dans `AppState`, la refonte backend-agnostique de `demo_sql`/`demo_config`/splash et les targets `ks-store.pg.*` ; +- définit la façade de stockage backend-agnostique, l'encapsulation de PostgreSQL, la stratégie de configuration backend opaque jusqu'à `ks-store`, le résumé runtime/init sans secrets, la future réutilisation d'un `Store` persistant dans `AppState`, le renommage structurel `demo_sql_* -> demo_store_*`, la refonte backend-agnostique de `demo_config`/splash et le domaine de tracing PostgreSQL `ks-store.pg` sous l’unique target `ks-store` ; - ajoute [`docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md`](../docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md) ; - ne crée aucune table DEX/trading ni projection metadata N4 dans `0.5.3` ; - considère `pre.007` comme premier candidat de clôture et autorise explicitement son décalage si des manques apparaissent ; diff --git a/ks-store/README.md b/ks-store/README.md index db760b3..5c4b20f 100644 --- a/ks-store/README.md +++ b/ks-store/README.md @@ -1,48 +1,56 @@ - + # ks-store -`ks-store` consolide les contrats de stockage et l’implémentation PostgreSQL de `khadhroony-bot3`. +`ks-store` porte la frontière de persistance généraliste de Khadhroony Solana. Les consommateurs utilisent des contrats fonctionnels et une façade `Store` indépendante du moteur concret ; PostgreSQL reste l'implémentation opérationnelle de `0.5.3`, confinée à l'intérieur de la crate. ## Périmètre La crate expose : -- DTO et entités raw, observations, Core, décodage, matérialisation et ledger ; -- traits async de repositories indépendants du backend ; -- pagination et filtres bornés ; -- `PostgresStore` et ses options de connexion ; -- initialisation idempotente du schéma et migrations SQL ; -- diagnostics read-only, healthchecks et résumés de replay ; -- transactions atomiques pour extraction, décodage et matérialisation. +- `Store` et `StoreOpenOptions` comme façade d'ouverture et handle persistant backend-agnostique ; +- les DTO et entités raw, observations, Core, décodage, matérialisation et ledger ; +- les traits async `StoreHealthStore`, `RawTransactionStore`, `CoreTransactionStore`, `CoreExtractionStore` et `DecodePipelineStore` ; +- les filtres et résultats de replay sans préfixe backend ; +- des diagnostics et résumés runtime/init sanitisés ne révélant ni secret ni nom physique d'objet ; +- des transactions atomiques pour extraction, décodage et matérialisation. + +## Frontière backend + +`PostgresStore`, `PostgresStoreOptions`, `sqlx::PgPool`, les requêtes SQL et les noms physiques de tables sont privés à `ks-store`. `ks-pipeline`, les scénarios et les applications ne doivent connaître que `Store`, les repositories et les DTO fonctionnels. + +`ks-store` ne dépend pas de `ks-config`. Un consommateur lui fournit un backend sélectionné et un objet `backend_options` opaque ; seul `ks-store` interprète et valide les paramètres PostgreSQL puis crée la connexion. + +## Configuration et sécurité + +`StoreOpenOptions` ne dérive pas `Debug` et ne rend pas les options backend accessibles après construction. Les résumés publics exposent uniquement des informations explicitement sanitisées : code backend, présence d'une connexion, état d'initialisation, health, modèle logique et compteurs. + +Le backend PostgreSQL masque le descripteur de connexion et utilise l’unique target de crate `ks-store`, complété par `backend="postgres"`, `domain="ks-store.pg"` et un champ `action` structuré. Aucun DSN, password ou valeur bindée n'est destiné aux diagnostics publics. + +## Schéma pendant `0.5.3` + +`0.5.3-pre.002` ne modifie pas encore les tables héritées `kb_sol_*`. Leur reconstruction sous `k_sol_*`, les ressources SQL atomiques et le gel N1/N2/N3 commencent en `pre.003` conformément au plan actif. ## Responsabilités - garantir les invariants des données persistées ; -- séparer contrats et implémentation PostgreSQL à l’intérieur de la crate consolidée ; -- maintenir les tables canoniques `kb_sol_*` ; -- fournir au pipeline des frontières async typées ; +- posséder la connexion et l'initialisation du backend actif ; +- fournir des frontières async typées au pipeline ; - protéger les opérations multi-tables par transactions ; -- borner toutes les requêtes de diagnostic et de replay exposées. +- borner les diagnostics et replay exposés ; +- permettre le remplacement futur de PostgreSQL sans réécriture des consommateurs. ## Hors périmètre -La crate ne décode pas les instructions, n’acquiert pas les transactions et ne décide pas quelle matérialisation exécuter. Elle persiste les résultats produits par `ks-lib` et orchestrés par `ks-pipeline`. - -## API publique - -Les consommateurs utilisent principalement `PostgresStore`, `PostgresStoreOptions`, les traits `*Store`, les DTO `*Insert`/`*Row`, les filtres de replay et les diagnostics. Les constantes de tables et fonctions de validation sont publiques pour les audits et outils d’administration. +La crate ne décode pas les instructions, n'acquiert pas les transactions et ne décide pas quelle matérialisation ou projection N4 exécuter. Elle persiste les faits produits par `ks-lib` et orchestrés par `ks-pipeline`. ## Relations - dépend de `ks-core` pour les erreurs ; - utilise les contrats de replay de `ks-lib` ; -- est consommée principalement par `ks-pipeline`, les scénarios de démonstration et le desktop. - -## Statut - -Le stockage consolidé dispose de tests unitaires étendus et de tests PostgreSQL optionnels pilotés par environnement. Les opérations réelles exigent un serveur PostgreSQL et l’application préalable des migrations. +- est consommée principalement par `ks-pipeline`, `ks-pipeline-demo-scenarios` et `kb-app-demo-desktop` ; +- ne dépend pas de `ks-config`. ## Documents @@ -50,3 +58,4 @@ Le stockage consolidé dispose de tests unitaires étendus et de tests PostgreSQ - [Travaux restants](TODO.md) - [Historique](CHANGELOG.md) - [Architecture du stockage](../docs/architecture/STORAGE_ARCHITECTURE.md) +- [Plan actif `0.5.3`](../docs/plans/V0_5_3_KS_STORE_NORMALIZATION_PLAN.md) diff --git a/ks-store/TODO.md b/ks-store/TODO.md index 37374dc..01c62ed 100644 --- a/ks-store/TODO.md +++ b/ks-store/TODO.md @@ -1,18 +1,16 @@ - + # TODO — ks-store ## Série `0.5.x` -- [ ] `0.5.3` — introduire une façade `Store` backend-agnostique, rendre PostgreSQL/SQLx privés et supprimer les contrats publics historiques non implémentés. -- [ ] `0.5.3` — remodeler la configuration store afin que le backend sélectionné et ses options restent opaques jusqu'à leur validation/interprétation par `ks-store`. - [ ] `0.5.3` — reconstruire `kb_sol_* -> k_sol_*`, ajouter `block_time` aux ancres raw/Core, conserver top-level/CPI dans deux tables Core avec lifecycle/replay équivalents, `stack_height`/parent CPI audités et renommer le journal N3 en `k_sol_mat_outputs`. - [ ] `0.5.3` — déplacer les ressources sous `migrations/postgres/`, imposer une instruction SQL par fichier avec exécution privée via `include_str!`, séparer tables/keys/index/drop/truncate et supprimer le DDL dupliqué en Rust. - [ ] `0.5.3` — corriger l'invalidation descendante, formaliser les replays N1 -> N2 / N2 -> N3 / frontière N3 -> N4, les paginations/limites, les conversions BIGINT et les index justifiés par les requêtes réelles. -- [ ] `0.5.3` — migrer `ks-pipeline-demo-scenarios` et `kb-app-demo-desktop` vers les API génériques sans symboles `Postgres*`, conserver un `Store` générique dans l’état applicatif, fournir un résumé runtime/init sans secrets et réconcilier `demo_sql`/`demo_config`/splash sans noms d'objets physiques. +- [ ] `0.5.3` — finaliser en `pre.005` la présentation fonctionnelle des surfaces `demo_store_*`, `demo_config` et le splash autour des modèles/compteurs du nouveau baseline sans noms d'objets physiques ; le renommage technique `demo_sql_* -> demo_store_*` est réalisé en `pre.002`. - [ ] `0.5.3` — exécuter avant gel le canari future-decoder/Anchor-readiness sur N1/N2/N3 : bytes d'instruction, comptes ordonnés/flags, hiérarchie CPI, logs, `returnData`, observations/états de comptes et provenance decoder/schema ; ajouter seulement les contrats Solana génériques manquants, sans table `anchor_*`. -- [ ] `0.5.3` — produire le snapshot machine-readable des tables N1/N2/N3 réellement stabilisées après ce canari (13 tables existantes de baseline, plus éventuels contrats génériques nécessaires), documenter la politique N4 plus souple et les futures projections metadata asset/token + program metadata, puis ajouter les canaris anti-`kb_sol_*`, anti-backend leak, anti-secret et logging `ks-store.pg.*`. +- [ ] `0.5.3` — produire le snapshot machine-readable des tables N1/N2/N3 réellement stabilisées après ce canari (13 tables existantes de baseline, plus éventuels contrats génériques nécessaires), documenter la politique N4 plus souple et les futures projections metadata asset/token + program metadata, puis ajouter les canaris anti-`kb_sol_*`, anti-backend leak, anti-secret et logging sous le target canonique `ks-store` avec domaine structuré `ks-store.pg`. - [ ] PostgreSQL - auditer le dimensionnement du pool selon les charges réelles. - [ ] Résilience - décider et implémenter, si justifié, un retry borné pour les timeouts transitoires. - [ ] Administration - définir les outils supplémentaires prévus par le ROADMAP avant leur implémentation. diff --git a/ks-store/USAGE.md b/ks-store/USAGE.md index 9d2cb4e..5a7cf72 100644 --- a/ks-store/USAGE.md +++ b/ks-store/USAGE.md @@ -1,90 +1,79 @@ - + # Utilisation de ks-store -## Connexion PostgreSQL +## Ouvrir le store + +Le consommateur transmet uniquement le backend sélectionné et son objet d'options déjà résolu. Il ne construit jamais un pool PostgreSQL. ```rust -let options = match ks_store::PostgresStoreOptions::new( - database_url, - 10, - 10_000, +let options = match ks_store::StoreOpenOptions::new( true, + "postgres", + serde_json::json!({ + "url": database_url, + "max_connections": 10, + "connect_timeout_ms": 10_000, + "auto_initialize_schema": true + }), ) { Ok(value) => value, Err(error) => return Err(error), }; - -println!("postgres endpoint: {}", options.masked_dsn()); - -let store = match ks_store::PostgresStore::connect(options).await { +let store = match ks_store::Store::open(options).await { Ok(value) => value, Err(error) => return Err(error), }; ``` -`PostgresStoreOptions::new` valide le DSN, le nombre de connexions et le timeout. Lorsque `auto_initialize_schema` vaut `true`, la connexion applique les schémas idempotents. Utiliser `masked_dsn` ou `mask_postgres_dsn` dans les logs ; ne jamais journaliser le DSN brut. +`Store::open` interprète les options du backend actif, crée la connexion, exécute l'auto-initialisation lorsqu'elle est configurée et capture un résumé d'initialisation sanitisé. PostgreSQL reste le seul backend opérationnel exigé en `0.5.3-pre.002`; un code backend inconnu est refusé proprement. -## Diagnostics +## Résumés sûrs ```rust -let health = store.health_snapshot().await; -let migrations = store.migration_snapshot().await; -let backend = store.backend_diagnostics().await; - -println!("health={:?}", health.status); -println!("migration={:?}", migrations.status); -println!("backend={:?}", backend.descriptor.backend); -``` - -Les diagnostics de tables raw, Core et decode/materialization sont également disponibles par les méthodes `*_table_diagnostics`. - -```rust -let raw_tables = match store.raw_table_diagnostics().await { +let configuration = store.configuration_summary(); +let initialization = store.initialization_summary(); +let runtime = match store.runtime_summary().await { Ok(value) => value, Err(error) => return Err(error), }; +``` -for table in raw_tables { +Ces contrats ne contiennent pas les options backend brutes. Le descripteur de connexion éventuellement exposé par `runtime.backend` est masqué par le backend propriétaire. + +Le résumé d'initialisation classe les ressources par modèles logiques et fournit des compteurs. Il expose aussi `objects`, une liste de catégories physiques sans noms d'objets : PostgreSQL publie déjà le total `table`, tandis que `pre.003` ajoutera le total `index` avec le nouveau baseline. Les noms physiques de tables, index et contraintes restent internes au backend. + +## Diagnostics de ressources + +```rust +let raw_resources = match store.raw_resource_diagnostics().await { + Ok(value) => value, + Err(error) => return Err(error), +}; +for resource in raw_resources { println!( - "table={} domain={} exists={}", - table.table_name, - table.domain, - table.exists + "resource={} model={} available={}", + resource.resource_code, + resource.model_code, + resource.available ); } ``` -## Pagination bornée - -```rust -let first_page = ks_store::PageRequest::first_page(); - -let next_page = match ks_store::PageRequest::new(250, 250) { - Ok(value) => value, - Err(error) => return Err(error), -}; - -assert_eq!(first_page.limit, ks_store::DEFAULT_PAGE_SIZE); -assert!(next_page.limit <= ks_store::MAX_PAGE_SIZE); -``` +Les méthodes `core_resource_diagnostics`, `decode_resource_diagnostics` et `known_resource_diagnostics` suivent le même contrat backend-agnostique. ## Contrats de repositories -Les traits publics principaux sont : +Les traits publics effectivement implémentés sont : - `StoreHealthStore` ; - `RawTransactionStore` ; - `CoreTransactionStore` ; - `CoreExtractionStore` ; -- `DecodePipelineStore` ; -- `ProgramObservationStore` ; -- `DecodedEventStore` ; -- `MaterializedEventStore` ; -- `ProcessingLedgerStore`. +- `DecodePipelineStore`. -Ils permettent au pipeline de dépendre d’un contrat async plutôt que d’une requête SQL directe. +`Store` implémente ces traits et délègue au backend privé. Les anciens traits historiques sans implémentation réelle ont été retirés avant gel de l'API. ## Replay et requêtes bornées @@ -93,71 +82,43 @@ let candidates = match store.replay_transaction_candidates(filter).await { Ok(value) => value, Err(error) => return Err(error), }; - -for candidate in candidates { - println!( - "signature={} slot={}", - candidate.signature, - candidate.slot - ); -} -``` - -```rust let programs = match store.replay_program_summaries(program_filter).await { Ok(value) => value, Err(error) => return Err(error), }; - let entities = match store.replay_entity_summaries(entity_filter).await { Ok(value) => value, Err(error) => return Err(error), }; - -println!("programs={}, entities={}", programs.len(), entities.len()); ``` -Les filtres refusent les limites nulles ou supérieures aux bornes publiques. `PageRequest` impose également `DEFAULT_PAGE_SIZE` et `MAX_PAGE_SIZE`. +Les contrats publics utilisent `ReplayTransaction*`, `ReplayProgram*` et `ReplayEntity*`. Le scope d'instruction utilise désormais `TopLevel`, `Inner` et `Logs`; le terme historique `outer` n'est pas introduit dans la nouvelle API. + +## Pagination bornée + +```rust +let first_page = ks_store::PageRequest::first_page(); +let next_page = match ks_store::PageRequest::new(250, 250) { + Ok(value) => value, + Err(error) => return Err(error), +}; +assert_eq!(first_page.limit, ks_store::DEFAULT_PAGE_SIZE); +assert!(next_page.limit <= ks_store::MAX_PAGE_SIZE); +``` + +La refonte cursorisée prévue par le plan `0.5.3` reste dans la tranche repositories/replay ultérieure. ## Transactions atomiques -Les bundles `CoreExtractionBundle`, `DecodePersistenceBundle` et `MaterializationPersistenceBundle` regroupent les écritures qui doivent réussir ou être annulées ensemble. Les consommateurs ne doivent pas reproduire manuellement ces transactions avec des écritures isolées. +Les bundles `CoreExtractionBundle`, `DecodePersistenceBundle` et `MaterializationPersistenceBundle` regroupent les écritures qui doivent réussir ou être annulées ensemble. Les consommateurs ne doivent pas reproduire manuellement ces transactions avec des écritures SQL isolées. -## Validation des noms de tables +## Tests PostgreSQL réels -```rust -if let Err(error) = ks_store::validate_raw_store_table_names() { - return Err(error); -} -if let Err(error) = ks_store::validate_core_store_table_names() { - return Err(error); -} -if let Err(error) = ks_store::validate_decode_store_table_names() { - return Err(error); -} +Les tests opt-in utilisent `KS_SECRET_POSTGRES_TEST_URL`. Aucun credential réel ne doit être enregistré dans les fixtures ou logs. Les erreurs d'ouverture de connexion sont volontairement sanitisées. -assert!(ks_store::is_valid_solana_table_name( - ks_store::CORE_TRANSACTIONS_TABLE_NAME -)); -``` +## Limites de `pre.002` -## Schéma et tables - -Les constantes `RAW_STORE_TABLE_NAMES`, `CORE_STORE_TABLE_NAMES` et `DECODE_STORE_TABLE_NAMES` exposent les noms canoniques. Les fonctions `validate_*_table_names` et `is_valid_solana_table_name` servent aux audits et outils d’administration. - -## Erreurs et invariants - -Toutes les APIs utilisent `ks_core::Result`. Les DTO valident notamment les signatures, chemins, Program IDs, clés, montants, états de traitement et identités de ledger. Les erreurs de contrat peuvent être construites avec `storage_contract_error`. - -## Tests instructifs - -- les tests `*_rejects_*` documentent les invariants des DTO et filtres ; -- `decoded_events_and_ledger_roll_back_together` vérifie l’atomicité ; -- `optional_postgres_*_from_env` couvre les parcours réels lorsque l’environnement PostgreSQL est configuré ; -- les tests de schéma vérifient les noms, index, contraintes et migrations. - -## Limites - -- backend actif : PostgreSQL ; -- les tests réels sont optionnels sans DSN de test ; -- la crate ne prend aucune décision de décodage ou de matérialisation. +- les tables restent temporairement sous leur namespace historique jusqu'à `pre.003` ; +- les migrations PostgreSQL ne sont pas encore déplacées sous `migrations/postgres/` ; +- la pagination cursorisée et l'invalidation descendante sont traitées dans la tranche repositories/replay ; +- la refonte complète des fenêtres desktop `demo_store_*` est prévue dans la tranche consommateurs. diff --git a/ks-store/src/constants.rs b/ks-store/src/constants.rs index f7136f0..f8d0b43 100644 --- a/ks-store/src/constants.rs +++ b/ks-store/src/constants.rs @@ -1,7 +1,9 @@ // file: ks-store/src/constants.rs -// version: 2 +// version: 5 //! Local constants for the `ks-store` crate. -/// Canonical tracing target for this crate. +/// Canonical tracing target for backend-independent storage operations. pub(crate) const TRACING_TARGET: &str = "ks-store"; +/// Transitional schema contract identifier used until the `pre.003` baseline rebuild. +pub const STORE_SCHEMA_CONTRACT_VERSION: &str = "0.5.3-pre.2-legacy-schema"; diff --git a/ks-store/src/contracts.rs b/ks-store/src/contracts.rs index 2a71645..85bcf6f 100644 --- a/ks-store/src/contracts.rs +++ b/ks-store/src/contracts.rs @@ -1,5 +1,5 @@ // file: ks-store/src/contracts.rs -// version: 3 +// version: 6 //! Backend-neutral storage contracts used by pipeline crates. @@ -8,6 +8,7 @@ mod entity; mod error; mod health; mod pagination; +mod replay; mod repository; /// Core account key insert contract. @@ -52,8 +53,6 @@ pub use self::dto::DecodeObservationInsert; pub use self::dto::DecodePersistenceBundle; /// Bounded contextual instruction selection filter for decode campaigns. pub use self::dto::DecodeSelectionFilter; -/// Decoded event insert contract. -pub use self::dto::DecodedEventInsert; /// Insert or upsert result contract returned by repositories. pub use self::dto::InsertOutcome; /// Maximum number of materialized rows returned by one bounded query. @@ -62,16 +61,12 @@ pub use self::dto::MAX_MATERIALIZED_EVENT_QUERY_ROWS; pub use self::dto::MaterializationPersistenceBundle; /// Bounded read-only materialized event selection. pub use self::dto::MaterializedEventFilter; -/// Materialized event insert contract. -pub use self::dto::MaterializedEventInsert; /// One materialized output returned by a bounded query. pub use self::dto::MaterializedEventQueryRow; /// One processor-owned materialized output row. pub use self::dto::MaterializedOutputInsert; /// Stable processing ledger identity. pub use self::dto::ProcessingLedgerIdentity; -/// Processing ledger mark request contract. -pub use self::dto::ProcessingLedgerMark; /// Stable processing ledger status. pub use self::dto::ProcessingLedgerStatus; /// Raw payload lifecycle mark request. @@ -84,12 +79,30 @@ pub use self::dto::RawPayloadRetentionState; pub use self::dto::RawTransactionInsert; /// Store backend diagnostic contract. pub use self::dto::StoreBackendDescriptor; -/// Store backend kind contract. -pub use self::dto::StoreBackendKind; +/// Backend-neutral diagnostic snapshot. +pub use self::dto::StoreBackendDiagnostics; +/// Store configuration summary safe for UI and logs. +pub use self::dto::StoreConfigurationSummary; +/// Store initialization status. +pub use self::dto::StoreInitializationStatus; +/// Initialization and model-verification summary captured while opening a store. +pub use self::dto::StoreInitializationSummary; /// Store migration diagnostic snapshot contract. pub use self::dto::StoreMigrationSnapshot; /// Store migration status contract. pub use self::dto::StoreMigrationStatus; +/// Store model verification status. +pub use self::dto::StoreModelVerificationStatus; +/// Verification summary for one logical store model. +pub use self::dto::StoreModelVerificationSummary; +/// Count summary for one backend object category without exposing physical names. +pub use self::dto::StoreObjectVerificationSummary; +/// Read-only diagnostics for one logical store resource. +pub use self::dto::StoreResourceDiagnostics; +/// Read-only statistics for one logical store resource. +pub use self::dto::StoreResourceStatistics; +/// Complete runtime summary safe for application diagnostics. +pub use self::dto::StoreRuntimeSummary; /// Transaction acquisition observation insert contract. pub use self::dto::TransactionObservationInsert; /// Transaction acquisition observation origin. @@ -108,12 +121,6 @@ pub use self::entity::CoreInstructionRow; pub use self::entity::CoreLogRow; /// Core transaction SQL-like row contract. pub use self::entity::CoreTransactionRow; -/// Decoded event SQL-like row contract. -pub use self::entity::DecodedEventRow; -/// Materialized event SQL-like row contract. -pub use self::entity::MaterializedEventRow; -/// Processing ledger SQL-like row contract. -pub use self::entity::ProcessingLedgerRow; /// Canonical raw Solana transaction SQL-like row contract. pub use self::entity::RawTransactionRow; /// Transaction acquisition observation SQL-like row contract. @@ -132,20 +139,30 @@ pub use self::pagination::MAX_PAGE_SIZE; pub use self::pagination::PageRequest; /// Sort direction for repository list operations. pub use self::pagination::SortDirection; +/// Maximum number of rows returned by one replay candidate query. +pub use self::replay::MAX_REPLAY_CANDIDATE_ROWS; +/// Bounded read-only filter for core entity summaries. +pub use self::replay::ReplayEntityFilter; +/// Core entity kind used while filtering replay candidates. +pub use self::replay::ReplayEntityKind; +/// Aggregated mint, owner or account-key occurrences from Core facts. +pub use self::replay::ReplayEntitySummary; +/// Bounded read-only filter for program summaries. +pub use self::replay::ReplayProgramFilter; +/// Program occurrence scope used while filtering replay candidates. +pub use self::replay::ReplayProgramScope; +/// Aggregated program occurrences across top-level instructions, inner instructions and linked logs. +pub use self::replay::ReplayProgramSummary; +/// One raw transaction candidate enriched with Core and processing-ledger diagnostics. +pub use self::replay::ReplayTransactionCandidate; +/// Bounded read-only filter for transaction replay candidates. +pub use self::replay::ReplayTransactionFilter; /// Canonical transaction to core extraction storage behavior. pub use self::repository::CoreExtractionStore; /// Core Solana storage behavior. pub use self::repository::CoreTransactionStore; /// Contextual instruction decode and materialization storage behavior. pub use self::repository::DecodePipelineStore; -/// Decoded event storage behavior. -pub use self::repository::DecodedEventStore; -/// Materialized event storage behavior. -pub use self::repository::MaterializedEventStore; -/// Processing ledger storage behavior. -pub use self::repository::ProcessingLedgerStore; -/// Program observation storage behavior. -pub use self::repository::ProgramObservationStore; /// Raw transaction storage behavior. pub use self::repository::RawTransactionStore; /// Store health storage behavior. diff --git a/ks-store/src/contracts/dto.rs b/ks-store/src/contracts/dto.rs index 567a352..16d4642 100644 --- a/ks-store/src/contracts/dto.rs +++ b/ks-store/src/contracts/dto.rs @@ -1,13 +1,12 @@ // file: ks-store/src/contracts/dto.rs -// version: 3 +// version: 6 //! Backend-neutral DTO exports for storage repository contracts. mod core; mod core_extraction; mod decode; -mod event; -mod ledger; +mod outcome; mod raw; mod store; @@ -67,14 +66,8 @@ pub use self::decode::MaterializedEventFilter; pub use self::decode::MaterializedEventQueryRow; /// One processor-owned materialized output row. pub use self::decode::MaterializedOutputInsert; -/// Decoded event insert contract. -pub use self::event::DecodedEventInsert; /// Insert or upsert result contract returned by repositories. -pub use self::event::InsertOutcome; -/// Materialized event insert contract. -pub use self::event::MaterializedEventInsert; -/// Processing ledger mark request contract. -pub use self::ledger::ProcessingLedgerMark; +pub use self::outcome::InsertOutcome; /// Raw payload lifecycle mark request. pub use self::raw::RawPayloadLifecycleMark; /// Raw payload processing state. @@ -91,9 +84,27 @@ pub use self::raw::TransactionObservationOrigin; pub use self::raw::TransactionObservationStatus; /// Store backend diagnostic contract. pub use self::store::StoreBackendDescriptor; -/// Store backend kind contract. -pub use self::store::StoreBackendKind; +/// Backend-neutral diagnostic snapshot. +pub use self::store::StoreBackendDiagnostics; +/// Store configuration summary safe for UI and logs. +pub use self::store::StoreConfigurationSummary; +/// Store initialization status. +pub use self::store::StoreInitializationStatus; +/// Initialization and model-verification summary captured while opening a store. +pub use self::store::StoreInitializationSummary; /// Store migration diagnostic snapshot contract. pub use self::store::StoreMigrationSnapshot; /// Store migration status contract. pub use self::store::StoreMigrationStatus; +/// Store model verification status. +pub use self::store::StoreModelVerificationStatus; +/// Verification summary for one logical store model. +pub use self::store::StoreModelVerificationSummary; +/// Count summary for one backend object category without exposing physical names. +pub use self::store::StoreObjectVerificationSummary; +/// Read-only diagnostics for one logical store resource. +pub use self::store::StoreResourceDiagnostics; +/// Read-only statistics for one logical store resource. +pub use self::store::StoreResourceStatistics; +/// Complete runtime summary safe for application diagnostics. +pub use self::store::StoreRuntimeSummary; diff --git a/ks-store/src/contracts/dto/event.rs b/ks-store/src/contracts/dto/event.rs deleted file mode 100644 index fbdc09c..0000000 --- a/ks-store/src/contracts/dto/event.rs +++ /dev/null @@ -1,143 +0,0 @@ -// file: ks-store/src/contracts/dto/event.rs -// version: 2 - -//! Decoded and materialized event storage DTOs. - -/// Insert or upsert result contract returned by repositories. -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct InsertOutcome { - /// Number of rows inserted by the repository call. - pub inserted_count: u64, - /// Number of rows updated by the repository call. - pub updated_count: u64, - /// Number of rows skipped by the repository call. - pub skipped_count: u64, -} - -impl InsertOutcome { - /// Builds an insert outcome from explicit counters. - pub fn new(inserted_count: u64, updated_count: u64, skipped_count: u64) -> Self { - return Self { - inserted_count, - updated_count, - skipped_count, - }; - } - - /// Returns the sum of inserted, updated and skipped rows. - pub fn total_count(&self) -> u64 { - return self.inserted_count + self.updated_count + self.skipped_count; - } -} - -/// Decoded event insert contract. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct DecodedEventInsert { - /// Transaction signature as non-empty base58 text. - pub signature: std::string::String, - /// Transaction slot in Solana unsigned representation. - pub slot: u64, - /// Stable instruction path, for example `0` or `2/1`. - pub instruction_path: std::string::String, - /// Program id as non-empty base58 text. - pub program_id: std::string::String, - /// Protocol family code. - pub protocol_code: std::string::String, - /// Protocol surface code. - pub surface_code: std::string::String, - /// Canonical event code. - pub event_code: std::string::String, - /// Decoded payload JSON. - pub payload_json: serde_json::Value, -} - -impl DecodedEventInsert { - /// Builds a decoded event insert contract from the shared decoded model. - pub fn from_model( - event: &ks_lib::MdDecodedProtocolEvent, - payload_json: serde_json::Value, - ) -> ks_core::Result { - if event.signature.0.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "decoded event signature must not be empty", - )); - } - if event.program_id.0.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "decoded event program id must not be empty", - )); - } - return std::result::Result::Ok(Self { - signature: event.signature.0.clone(), - slot: event.slot.0, - instruction_path: event.instruction_path.0.clone(), - program_id: event.program_id.0.clone(), - protocol_code: event.protocol_code.0.clone(), - surface_code: event.surface_code.0.clone(), - event_code: event.event_code.0.clone(), - payload_json, - }); - } -} - -/// Materialized event insert contract. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct MaterializedEventInsert { - /// Source transaction signature as non-empty base58 text. - pub signature: std::string::String, - /// Source transaction slot in Solana unsigned representation. - pub slot: u64, - /// Materialized event family code. - pub materialized_family: std::string::String, - /// Materialized payload JSON. - pub payload_json: serde_json::Value, -} - -impl MaterializedEventInsert { - /// Builds a materialized event insert contract after minimal validation. - pub fn new( - signature: impl std::convert::Into, - slot: u64, - materialized_family: impl std::convert::Into, - payload_json: serde_json::Value, - ) -> ks_core::Result { - let signature_value = signature.into(); - let materialized_family_value = materialized_family.into(); - if signature_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "materialized event signature must not be empty", - )); - } - if materialized_family_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "materialized event family must not be empty", - )); - } - return std::result::Result::Ok(Self { - signature: signature_value, - slot, - materialized_family: materialized_family_value, - payload_json, - }); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn insert_outcome_total_counts_all_buckets() { - let outcome = crate::InsertOutcome::new(1, 2, 3); - assert_eq!(outcome.total_count(), 6); - } - - #[test] - fn materialized_event_rejects_empty_family() { - let result = crate::MaterializedEventInsert::new( - "abc", - 1, - " ", - serde_json::json!({"kind": "trade"}), - ); - assert!(result.is_err()); - } -} diff --git a/ks-store/src/contracts/dto/ledger.rs b/ks-store/src/contracts/dto/ledger.rs deleted file mode 100644 index 9095a21..0000000 --- a/ks-store/src/contracts/dto/ledger.rs +++ /dev/null @@ -1,73 +0,0 @@ -// file: ks-store/src/contracts/dto/ledger.rs -// version: 2 - -//! Processing ledger storage DTOs. - -/// Processing ledger mark request contract. -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct ProcessingLedgerMark { - /// Processing stage name, for example `raw_ingest`, `core_extract` or `decode`. - pub stage: std::string::String, - /// Processing module name. - pub module_name: std::string::String, - /// Processing module version. - pub module_version: std::string::String, - /// Stable input key, usually a signature or notification id. - pub input_key: std::string::String, -} - -impl ProcessingLedgerMark { - /// Builds a processing ledger mark request after minimal validation. - pub fn new( - stage: impl std::convert::Into, - module_name: impl std::convert::Into, - module_version: impl std::convert::Into, - input_key: impl std::convert::Into, - ) -> ks_core::Result { - let stage_value = stage.into(); - let module_name_value = module_name.into(); - let module_version_value = module_version.into(); - let input_key_value = input_key.into(); - if stage_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "processing ledger stage must not be empty", - )); - } - if module_name_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "processing ledger module name must not be empty", - )); - } - if module_version_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "processing ledger module version must not be empty", - )); - } - if input_key_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "processing ledger input key must not be empty", - )); - } - return std::result::Result::Ok(Self { - stage: stage_value, - module_name: module_name_value, - module_version: module_version_value, - input_key: input_key_value, - }); - } -} - -#[cfg(test)] -mod tests { - #[test] - fn ledger_mark_rejects_empty_input_key() { - let result = crate::ProcessingLedgerMark::new("decode", "module", "1", " "); - assert!(result.is_err()); - } - - #[test] - fn ledger_mark_accepts_minimal_values() { - let result = crate::ProcessingLedgerMark::new("decode", "module", "1", "signature"); - assert!(result.is_ok()); - } -} diff --git a/ks-store/src/contracts/dto/outcome.rs b/ks-store/src/contracts/dto/outcome.rs new file mode 100644 index 0000000..2012f5d --- /dev/null +++ b/ks-store/src/contracts/dto/outcome.rs @@ -0,0 +1,40 @@ +// file: ks-store/src/contracts/dto/outcome.rs +// version: 4 + +//! Generic repository operation outcome DTOs. + +/// Insert or upsert result contract returned by repositories. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct InsertOutcome { + /// Number of rows inserted by the repository call. + pub inserted_count: u64, + /// Number of rows updated by the repository call. + pub updated_count: u64, + /// Number of rows skipped by the repository call. + pub skipped_count: u64, +} + +impl crate::InsertOutcome { + /// Builds an insert outcome from explicit counters. + pub fn new(inserted_count: u64, updated_count: u64, skipped_count: u64) -> Self { + return Self { + inserted_count, + updated_count, + skipped_count, + }; + } + + /// Returns the sum of inserted, updated and skipped rows. + pub fn total_count(&self) -> u64 { + return self.inserted_count + self.updated_count + self.skipped_count; + } +} + +#[cfg(test)] +mod tests { + #[test] + fn insert_outcome_total_counts_all_buckets() { + let outcome = crate::InsertOutcome::new(1, 2, 3); + assert_eq!(outcome.total_count(), 6); + } +} diff --git a/ks-store/src/contracts/dto/store.rs b/ks-store/src/contracts/dto/store.rs index 3e60785..29a47b1 100644 --- a/ks-store/src/contracts/dto/store.rs +++ b/ks-store/src/contracts/dto/store.rs @@ -1,20 +1,7 @@ // file: ks-store/src/contracts/dto/store.rs -// version: 2 +// version: 5 -//! Store backend diagnostic DTOs. - -/// Store backend kind contract. -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub enum StoreBackendKind { - /// PostgreSQL backend. - Postgres, - /// SQLite backend retained for tests and legacy imports. - Sqlite, - /// In-memory backend used by offline tests. - Memory, - /// Backend is not known. - Unknown, -} +//! Backend-agnostic store configuration and diagnostic DTOs. /// Store migration status contract. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -33,38 +20,81 @@ pub enum StoreMigrationStatus { Failed, } -/// Store backend diagnostic contract. -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct StoreBackendDescriptor { - /// Store backend kind. - pub backend_kind: StoreBackendKind, - /// Human-readable backend label. - pub backend_label: std::string::String, - /// Masked DSN or connection descriptor safe for diagnostics. - pub masked_dsn: std::option::Option, - /// Current PostgreSQL schema or equivalent namespace when known. - pub current_schema: std::option::Option, +/// Store initialization status exposed without backend-specific details. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum StoreInitializationStatus { + /// Persistence is disabled for the selected profile. + Disabled, + /// Every expected logical model is available. + Ready, + /// The backend is reachable but at least one expected logical model is incomplete. + Partial, + /// Initialization or verification failed. + Failed, } -impl StoreBackendDescriptor { +/// Store model verification status. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum StoreModelVerificationStatus { + /// Every expected resource for the logical model is available. + Ready, + /// At least one expected resource is missing. + Incomplete, +} + +/// Store configuration summary safe for UI and logs. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreConfigurationSummary { + /// Whether persistence is enabled for the selected profile. + pub enabled: bool, + /// Stable backend code interpreted by `ks-store`. + pub backend_code: std::string::String, + /// Whether the selected backend supplied a connection/location value. + pub connection_configured: bool, + /// Whether schema auto-initialization is enabled for this backend. + pub auto_initialize_schema: bool, + /// Number of backend options supplied after configuration composition. + pub backend_option_count: u32, +} + +/// Store backend diagnostic contract safe for UI display. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreBackendDescriptor { + /// Stable backend code such as `postgres`. + pub backend_code: std::string::String, + /// Human-readable backend label. + pub backend_label: std::string::String, + /// Masked connection descriptor safe for diagnostics. + pub masked_connection_descriptor: std::option::Option, + /// Backend namespace, schema or equivalent logical location when known. + pub namespace: std::option::Option, +} + +impl crate::StoreBackendDescriptor { /// Builds a store backend descriptor after minimal validation. pub fn new( - backend_kind: StoreBackendKind, + backend_code: impl std::convert::Into, backend_label: impl std::convert::Into, - masked_dsn: std::option::Option, - current_schema: std::option::Option, + masked_connection_descriptor: std::option::Option, + namespace: std::option::Option, ) -> ks_core::Result { + let backend_code_value = backend_code.into(); let backend_label_value = backend_label.into(); + if backend_code_value.trim().is_empty() { + return std::result::Result::Err(ks_core::Error::db( + "store backend code must not be empty", + )); + } if backend_label_value.trim().is_empty() { return std::result::Result::Err(ks_core::Error::db( "store backend label must not be empty", )); } return std::result::Result::Ok(Self { - backend_kind, + backend_code: backend_code_value, backend_label: backend_label_value, - masked_dsn, - current_schema, + masked_connection_descriptor, + namespace, }); } } @@ -73,7 +103,7 @@ impl StoreBackendDescriptor { #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct StoreMigrationSnapshot { /// Current migration status. - pub status: StoreMigrationStatus, + pub status: crate::StoreMigrationStatus, /// Last applied migration identifier when known. pub current_version: std::option::Option, /// Pending migration identifiers when known. @@ -82,10 +112,10 @@ pub struct StoreMigrationSnapshot { pub message: std::option::Option, } -impl StoreMigrationSnapshot { +impl crate::StoreMigrationSnapshot { /// Builds a migration snapshot from explicit values. pub fn new( - status: StoreMigrationStatus, + status: crate::StoreMigrationStatus, current_version: std::option::Option, pending_versions: std::vec::Vec, message: std::option::Option, @@ -99,13 +129,114 @@ impl StoreMigrationSnapshot { } } +/// Backend-neutral diagnostic snapshot. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreBackendDiagnostics { + /// Backend descriptor safe for UI display. + pub descriptor: crate::StoreBackendDescriptor, + /// Backend health snapshot. + pub health: crate::StoreHealthSnapshot, + /// Migration status snapshot. + pub migrations: crate::StoreMigrationSnapshot, + /// Backend version string when the backend exposes one. + pub backend_version: std::option::Option, +} + +/// Read-only statistics for one logical store resource. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreResourceStatistics { + /// Number of records currently stored in the resource. + pub record_count: i64, + /// Lowest observed Solana slot when meaningful for the resource. + pub min_slot: std::option::Option, + /// Highest observed Solana slot when meaningful for the resource. + pub max_slot: std::option::Option, + /// Latest creation timestamp rendered by the backend for diagnostics. + pub latest_created_at: std::option::Option, +} + +/// Read-only diagnostics for one logical store resource. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreResourceDiagnostics { + /// Stable backend-independent resource code. + pub resource_code: std::string::String, + /// Logical model containing this resource. + pub model_code: std::string::String, + /// Human-readable resource role. + pub role: std::string::String, + /// Whether the resource is available in the active backend. + pub available: bool, + /// Resource statistics when available and supported. + pub statistics: std::option::Option, +} + +/// Verification summary for one logical store model. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreModelVerificationSummary { + /// Stable logical model code such as `raw`, `core`, `processing`, `decode` or `materialization`. + pub model_code: std::string::String, + /// Verification status for the model. + pub status: crate::StoreModelVerificationStatus, + /// Number of resources expected by the current store contract. + pub expected_resource_count: u32, + /// Number of expected resources available after verification. + pub available_resource_count: u32, + /// Number of resources created during this initialization pass. + pub created_resource_count: u32, + /// Number of expected resources still missing. + pub missing_resource_count: u32, +} + +/// Count summary for one backend object category without exposing physical object names. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreObjectVerificationSummary { + /// Stable object category such as `table`, `index` or a backend-specific equivalent. + pub object_kind: std::string::String, + /// Number of objects expected by the active backend contract. + pub expected_count: u32, + /// Number of expected objects available after verification. + pub available_count: u32, + /// Number of expected objects created during this initialization pass. + pub created_count: u32, +} + +/// Initialization and model-verification summary captured while opening a store. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreInitializationSummary { + /// Overall initialization status. + pub status: crate::StoreInitializationStatus, + /// Stable schema/model contract version. + pub schema_contract_version: std::string::String, + /// Per-model verification summaries. + pub models: std::vec::Vec, + /// Total number of expected logical resources. + pub expected_resource_count: u32, + /// Total number of available logical resources. + pub available_resource_count: u32, + /// Total number of logical resources created during opening. + pub created_resource_count: u32, + /// Backend object counts grouped by category without physical object names. + pub objects: std::vec::Vec, +} + +/// Complete runtime summary safe for desktop diagnostics. +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct StoreRuntimeSummary { + /// Sanitized selected configuration. + pub configuration: crate::StoreConfigurationSummary, + /// Backend diagnostics when persistence is enabled and opened. + pub backend: std::option::Option, + /// Initialization/model verification result captured during opening. + pub initialization: crate::StoreInitializationSummary, +} + #[cfg(test)] mod tests { #[test] - fn store_descriptor_rejects_empty_label() { + fn store_descriptor_rejects_empty_code() { let result = crate::StoreBackendDescriptor::new( - crate::StoreBackendKind::Postgres, " ", + "PostgreSQL", std::option::Option::None, std::option::Option::None, ); diff --git a/ks-store/src/contracts/entity.rs b/ks-store/src/contracts/entity.rs index 1baa5a6..45e17e4 100644 --- a/ks-store/src/contracts/entity.rs +++ b/ks-store/src/contracts/entity.rs @@ -1,32 +1,24 @@ // file: ks-store/src/contracts/entity.rs -// version: 3 +// version: 5 -//! Backend-neutral SQL-like entity exports for storage adapters. +//! Backend-neutral persisted entity exports for storage adapters. mod core; -mod event; -mod ledger; mod raw; -/// Core account key SQL-like row contract. +/// Core account key persisted row contract. pub use self::core::CoreAccountKeyRow; -/// Core balance change SQL-like row contract. +/// Core balance change persisted row contract. pub use self::core::CoreBalanceChangeRow; -/// Core inner instruction SQL-like row contract. +/// Core inner instruction persisted row contract. pub use self::core::CoreInnerInstructionRow; -/// Core instruction SQL-like row contract. +/// Core instruction persisted row contract. pub use self::core::CoreInstructionRow; -/// Core log SQL-like row contract. +/// Core log persisted row contract. pub use self::core::CoreLogRow; -/// Core transaction SQL-like row contract. +/// Core transaction persisted row contract. pub use self::core::CoreTransactionRow; -/// Decoded event SQL-like row contract. -pub use self::event::DecodedEventRow; -/// Materialized event SQL-like row contract. -pub use self::event::MaterializedEventRow; -/// Processing ledger SQL-like row contract. -pub use self::ledger::ProcessingLedgerRow; -/// Canonical raw Solana transaction SQL-like row contract. +/// Canonical raw Solana transaction persisted row contract. pub use self::raw::RawTransactionRow; -/// Transaction acquisition observation SQL-like row contract. +/// Transaction acquisition observation persisted row contract. pub use self::raw::TransactionObservationRow; diff --git a/ks-store/src/contracts/entity/core.rs b/ks-store/src/contracts/entity/core.rs index bea4adf..016f2f2 100644 --- a/ks-store/src/contracts/entity/core.rs +++ b/ks-store/src/contracts/entity/core.rs @@ -1,9 +1,9 @@ // file: ks-store/src/contracts/entity/core.rs -// version: 2 +// version: 3 -//! Core Solana SQL-like entities. +//! Core Solana persisted entities. -/// Core transaction SQL-like row contract. +/// Core transaction persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CoreTransactionRow { /// Technical primary key. @@ -24,7 +24,7 @@ pub struct CoreTransactionRow { pub updated_at: chrono::DateTime, } -/// Core account key SQL-like row contract. +/// Core account key persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CoreAccountKeyRow { /// Technical primary key. @@ -51,7 +51,7 @@ pub struct CoreAccountKeyRow { pub created_at: chrono::DateTime, } -/// Core instruction SQL-like row contract. +/// Core instruction persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CoreInstructionRow { /// Technical primary key. @@ -80,7 +80,7 @@ pub struct CoreInstructionRow { pub updated_at: chrono::DateTime, } -/// Core inner instruction SQL-like row contract. +/// Core inner instruction persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CoreInnerInstructionRow { /// Technical primary key. @@ -107,7 +107,7 @@ pub struct CoreInnerInstructionRow { pub created_at: chrono::DateTime, } -/// Core log SQL-like row contract. +/// Core log persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CoreLogRow { /// Technical primary key. @@ -132,7 +132,7 @@ pub struct CoreLogRow { pub created_at: chrono::DateTime, } -/// Core balance change SQL-like row contract. +/// Core balance change persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct CoreBalanceChangeRow { /// Technical primary key. diff --git a/ks-store/src/contracts/entity/event.rs b/ks-store/src/contracts/entity/event.rs deleted file mode 100644 index f421ba4..0000000 --- a/ks-store/src/contracts/entity/event.rs +++ /dev/null @@ -1,46 +0,0 @@ -// file: ks-store/src/contracts/entity/event.rs -// version: 2 - -//! Decoded and materialized event SQL-like entities. - -/// Decoded event SQL-like row contract. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct DecodedEventRow { - /// Technical primary key. - pub id: i64, - /// Transaction signature as non-empty base58 text. - pub signature: std::string::String, - /// Transaction slot stored as SQL `BIGINT`. - pub slot: i64, - /// Stable instruction path, for example `0` or `2/1`. - pub instruction_path: std::string::String, - /// Program id as non-empty base58 text. - pub program_id: std::string::String, - /// Protocol family code. - pub protocol_code: std::string::String, - /// Protocol surface code. - pub surface_code: std::string::String, - /// Canonical event code. - pub event_code: std::string::String, - /// Decoded payload JSON. - pub payload_json: serde_json::Value, - /// Insert timestamp. - pub created_at: chrono::DateTime, -} - -/// Materialized event SQL-like row contract. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct MaterializedEventRow { - /// Technical primary key. - pub id: i64, - /// Source transaction signature as non-empty base58 text. - pub signature: std::string::String, - /// Source transaction slot stored as SQL `BIGINT`. - pub slot: i64, - /// Materialized event family code. - pub materialized_family: std::string::String, - /// Materialized payload JSON. - pub payload_json: serde_json::Value, - /// Insert timestamp. - pub created_at: chrono::DateTime, -} diff --git a/ks-store/src/contracts/entity/ledger.rs b/ks-store/src/contracts/entity/ledger.rs deleted file mode 100644 index 4035462..0000000 --- a/ks-store/src/contracts/entity/ledger.rs +++ /dev/null @@ -1,37 +0,0 @@ -// file: ks-store/src/contracts/entity/ledger.rs -// version: 2 - -//! Processing ledger SQL-like entities. - -/// Processing ledger SQL-like row contract. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct ProcessingLedgerRow { - /// Technical primary key. - pub id: i64, - /// Processing stage name. - pub stage: std::string::String, - /// Processor implementation name. - pub processor_name: std::string::String, - /// Processor semantic version. - pub processor_version: std::string::String, - /// Stable input key, usually a transaction signature. - pub input_key: std::string::String, - /// Deterministic input hash. - pub input_hash: std::string::String, - /// Current processing status. - pub status: crate::ProcessingLedgerStatus, - /// Number of started attempts for this processor identity. - pub attempt_count: i32, - /// Optional start timestamp for the latest attempt. - pub started_at: std::option::Option>, - /// Optional finish timestamp for the latest terminal attempt. - pub finished_at: std::option::Option>, - /// Optional machine-readable error code. - pub error_code: std::option::Option, - /// Optional diagnostic error message. - pub error_message: std::option::Option, - /// Insert timestamp. - pub created_at: chrono::DateTime, - /// Last update timestamp. - pub updated_at: chrono::DateTime, -} diff --git a/ks-store/src/contracts/entity/raw.rs b/ks-store/src/contracts/entity/raw.rs index 26e9d52..4f489d5 100644 --- a/ks-store/src/contracts/entity/raw.rs +++ b/ks-store/src/contracts/entity/raw.rs @@ -1,9 +1,9 @@ // file: ks-store/src/contracts/entity/raw.rs -// version: 2 +// version: 3 -//! Canonical Solana transaction and acquisition observation SQL-like entities. +//! Canonical Solana transaction and acquisition observation persisted entities. -/// Canonical raw Solana transaction SQL-like row contract. +/// Canonical raw Solana transaction persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct RawTransactionRow { /// Technical primary key. @@ -28,7 +28,7 @@ pub struct RawTransactionRow { pub updated_at: chrono::DateTime, } -/// Transaction acquisition observation SQL-like row contract. +/// Transaction acquisition observation persisted row contract. #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct TransactionObservationRow { /// Technical primary key. diff --git a/ks-store/src/postgres/replay_candidates.rs b/ks-store/src/contracts/replay.rs similarity index 87% rename from ks-store/src/postgres/replay_candidates.rs rename to ks-store/src/contracts/replay.rs index 04a1d8f..591b653 100644 --- a/ks-store/src/postgres/replay_candidates.rs +++ b/ks-store/src/contracts/replay.rs @@ -1,30 +1,30 @@ -// file: ks-store/src/postgres/replay_candidates.rs -// version: 3 +// file: ks-store/src/contracts/replay.rs +// version: 6 -//! Read-only replay candidate filters and PostgreSQL result rows. +//! Backend-agnostic replay candidate filters and read models. /// Maximum number of rows returned by one replay candidate query. pub const MAX_REPLAY_CANDIDATE_ROWS: u32 = 100_000; /// Program occurrence scope used while filtering replay candidates. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum PostgresReplayProgramScope { - /// Match outer instructions, inner instructions or reliably linked logs. +pub enum ReplayProgramScope { + /// Match top-level instructions, inner instructions or reliably linked logs. Any, /// Match only top-level instructions. - Outer, + TopLevel, /// Match only inner instructions. Inner, /// Match only logs with a reliably linked program id. Logs, } -impl PostgresReplayProgramScope { +impl crate::ReplayProgramScope { /// Returns the stable SQL code for this scope. pub fn as_sql(self) -> &'static str { return match self { Self::Any => "any", - Self::Outer => "outer", + Self::TopLevel => "top_level", Self::Inner => "inner", Self::Logs => "logs", }; @@ -33,7 +33,7 @@ impl PostgresReplayProgramScope { /// Core entity kind used while filtering replay candidates. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum PostgresReplayEntityKind { +pub enum ReplayEntityKind { /// SPL or Token-2022 mint address. Mint, /// Token account owner address. @@ -42,7 +42,7 @@ pub enum PostgresReplayEntityKind { AccountKey, } -impl PostgresReplayEntityKind { +impl crate::ReplayEntityKind { /// Returns the stable SQL code for this entity kind. pub fn as_sql(self) -> &'static str { return match self { @@ -55,7 +55,7 @@ impl PostgresReplayEntityKind { /// Bounded read-only filter for transaction replay candidates. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct PostgresReplayTransactionFilter { +pub struct ReplayTransactionFilter { /// Optional partial signature search. pub signature_contains: std::option::Option, /// Optional inclusive minimum slot. @@ -69,9 +69,9 @@ pub struct PostgresReplayTransactionFilter { /// Optional exact program id. pub program_id: std::option::Option, /// Program occurrence scope. - pub program_scope: crate::PostgresReplayProgramScope, + pub program_scope: crate::ReplayProgramScope, /// Optional core entity kind. - pub entity_kind: std::option::Option, + pub entity_kind: std::option::Option, /// Optional exact entity value. pub entity_value: std::option::Option, /// Maximum returned rows. @@ -80,9 +80,8 @@ pub struct PostgresReplayTransactionFilter { pub newest_first: bool, } -impl PostgresReplayTransactionFilter { +impl crate::ReplayTransactionFilter { /// Creates and validates a bounded transaction candidate filter. - #[allow(clippy::too_many_arguments)] pub fn new( signature_contains: std::option::Option, min_slot: std::option::Option, @@ -90,8 +89,8 @@ impl PostgresReplayTransactionFilter { raw_processing_state: std::option::Option, ledger_status: std::option::Option, program_id: std::option::Option, - program_scope: crate::PostgresReplayProgramScope, - entity_kind: std::option::Option, + program_scope: crate::ReplayProgramScope, + entity_kind: std::option::Option, entity_value: std::option::Option, limit: u32, newest_first: bool, @@ -148,7 +147,7 @@ impl PostgresReplayTransactionFilter { /// One raw transaction candidate enriched with core and ledger diagnostics. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct PostgresReplayTransactionCandidate { +pub struct ReplayTransactionCandidate { /// Canonical transaction signature. pub signature: std::string::String, /// Transaction slot. @@ -168,27 +167,27 @@ pub struct PostgresReplayTransactionCandidate { /// Latest core extraction attempt count. pub attempt_count: i32, /// Number of top-level instructions. - pub outer_instruction_count: i64, + pub top_level_instruction_count: i64, /// Number of inner instructions. pub inner_instruction_count: i64, /// Number of distinct top-level programs. - pub outer_program_count: i64, + pub top_level_program_count: i64, /// Number of distinct inner programs. pub inner_program_count: i64, - /// Raw row update timestamp rendered by PostgreSQL. + /// Raw row update timestamp rendered by the active backend. pub updated_at: std::string::String, } /// Bounded read-only filter for program summaries. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct PostgresReplayProgramFilter { +pub struct ReplayProgramFilter { /// Optional partial program id search. pub program_id_contains: std::option::Option, /// Maximum returned rows. pub limit: u32, } -impl PostgresReplayProgramFilter { +impl crate::ReplayProgramFilter { /// Creates and validates a bounded program summary filter. pub fn new( program_id_contains: std::option::Option, @@ -205,15 +204,15 @@ impl PostgresReplayProgramFilter { } } -/// Aggregated program occurrences across outer, inner and linked logs. +/// Aggregated program occurrences across top-level, inner and linked logs. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct PostgresReplayProgramSummary { +pub struct ReplayProgramSummary { /// Program id. pub program_id: std::string::String, /// Number of distinct transactions containing the program. pub transaction_count: i64, /// Number of top-level instruction occurrences. - pub outer_instruction_count: i64, + pub top_level_instruction_count: i64, /// Number of inner instruction occurrences. pub inner_instruction_count: i64, /// Number of reliably linked log occurrences. @@ -226,19 +225,19 @@ pub struct PostgresReplayProgramSummary { /// Bounded read-only filter for core entity summaries. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct PostgresReplayEntityFilter { +pub struct ReplayEntityFilter { /// Entity kind to aggregate. - pub entity_kind: crate::PostgresReplayEntityKind, + pub entity_kind: crate::ReplayEntityKind, /// Optional partial entity value search. pub entity_value_contains: std::option::Option, /// Maximum returned rows. pub limit: u32, } -impl PostgresReplayEntityFilter { +impl crate::ReplayEntityFilter { /// Creates and validates a bounded entity summary filter. pub fn new( - entity_kind: crate::PostgresReplayEntityKind, + entity_kind: crate::ReplayEntityKind, entity_value_contains: std::option::Option, limit: u32, ) -> ks_core::Result { @@ -256,7 +255,7 @@ impl PostgresReplayEntityFilter { /// Aggregated mint, owner or account-key occurrences from core tables. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct PostgresReplayEntitySummary { +pub struct ReplayEntitySummary { /// Stable entity kind code. pub entity_kind: std::string::String, /// Mint, owner or account-key address. @@ -335,14 +334,14 @@ fn validate_optional_code( mod tests { #[test] fn transaction_filter_rejects_inverted_slots() { - let result = crate::PostgresReplayTransactionFilter::new( + let result = crate::ReplayTransactionFilter::new( std::option::Option::None, std::option::Option::Some(20), std::option::Option::Some(10), std::option::Option::None, std::option::Option::None, std::option::Option::None, - crate::PostgresReplayProgramScope::Any, + crate::ReplayProgramScope::Any, std::option::Option::None, std::option::Option::None, 100, @@ -353,15 +352,15 @@ mod tests { #[test] fn transaction_filter_requires_complete_entity_pair() { - let result = crate::PostgresReplayTransactionFilter::new( + let result = crate::ReplayTransactionFilter::new( std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None, - crate::PostgresReplayProgramScope::Any, - std::option::Option::Some(crate::PostgresReplayEntityKind::Mint), + crate::ReplayProgramScope::Any, + std::option::Option::Some(crate::ReplayEntityKind::Mint), std::option::Option::None, 100, true, @@ -371,7 +370,7 @@ mod tests { #[test] fn program_filter_rejects_limit_above_maximum() { - let result = crate::PostgresReplayProgramFilter::new( + let result = crate::ReplayProgramFilter::new( std::option::Option::None, crate::MAX_REPLAY_CANDIDATE_ROWS + 1, ); diff --git a/ks-store/src/contracts/repository.rs b/ks-store/src/contracts/repository.rs index 9569a9b..fd92587 100644 --- a/ks-store/src/contracts/repository.rs +++ b/ks-store/src/contracts/repository.rs @@ -1,5 +1,5 @@ // file: ks-store/src/contracts/repository.rs -// version: 2 +// version: 3 //! Storage trait definitions shared by concrete stores. @@ -192,46 +192,3 @@ pub trait DecodePipelineStore { filter: &crate::MaterializedEventFilter, ) -> ks_core::Result>; } - -/// Program observation storage behavior. -#[async_trait::async_trait] -pub trait ProgramObservationStore { - /// Stores program observations. - async fn store_observations( - &self, - observations: &[ks_lib::MdProgramObservation], - ) -> ks_core::Result; -} - -/// Decoded event storage behavior. -#[async_trait::async_trait] -pub trait DecodedEventStore { - /// Stores decoded protocol events. - async fn store_decoded_events( - &self, - events: &[crate::DecodedEventInsert], - ) -> ks_core::Result; -} - -/// Materialized event storage behavior. -#[async_trait::async_trait] -pub trait MaterializedEventStore { - /// Stores materialized business events. - async fn store_materialized_events( - &self, - events: &[crate::MaterializedEventInsert], - ) -> ks_core::Result; -} - -/// Processing ledger storage behavior. -#[async_trait::async_trait] -pub trait ProcessingLedgerStore { - /// Marks an input as processed for a module version. - async fn mark_processed( - &self, - mark: &crate::ProcessingLedgerMark, - ) -> ks_core::Result; - - /// Returns true when an input was already processed for a module version. - async fn is_processed(&self, mark: &crate::ProcessingLedgerMark) -> ks_core::Result; -} diff --git a/ks-store/src/lib.rs b/ks-store/src/lib.rs index d34186a..f219500 100644 --- a/ks-store/src/lib.rs +++ b/ks-store/src/lib.rs @@ -1,7 +1,7 @@ // file: ks-store/src/lib.rs -// version: 5 +// version: 10 -//! Backend-neutral storage contracts and PostgreSQL implementation. +//! Backend-agnostic Solana storage contracts and store facade. #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -9,13 +9,177 @@ mod constants; mod contracts; mod postgres; +mod store; -/// Canonical tracing target for storage operations. +/// Canonical crate-internal tracing target shared through the crate-root facade. pub(crate) use self::constants::TRACING_TARGET; +/// Crate-internal storage symbol `CORE_ACCOUNT_KEYS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::CORE_ACCOUNT_KEYS_TABLE_NAME; +/// Crate-internal storage symbol `CORE_BALANCE_CHANGES_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::CORE_BALANCE_CHANGES_TABLE_NAME; +/// Crate-internal storage symbol `CORE_INNER_INSTRUCTIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::CORE_INNER_INSTRUCTIONS_TABLE_NAME; +/// Crate-internal storage symbol `CORE_INSTRUCTIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::CORE_INSTRUCTIONS_TABLE_NAME; +/// Crate-internal storage symbol `CORE_LOGS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::CORE_LOGS_TABLE_NAME; +/// Crate-internal storage symbol `CORE_TRANSACTIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::CORE_TRANSACTIONS_TABLE_NAME; +/// Crate-internal storage symbol `DECODE_COVERAGE_DECLARATIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME; +/// Crate-internal storage symbol `DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME; +/// Crate-internal storage symbol `DECODE_EVENTS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::DECODE_EVENTS_TABLE_NAME; +/// Crate-internal storage symbol `MATERIALIZED_EVENTS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::MATERIALIZED_EVENTS_TABLE_NAME; +/// Crate-internal storage symbol `PROCESSING_LEDGER_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::PROCESSING_LEDGER_TABLE_NAME; +/// Crate-internal storage symbol `PostgresStore` shared through the crate-root facade. +pub(crate) use self::postgres::PostgresStore; +/// Crate-internal storage symbol `PostgresStoreOptions` shared through the crate-root facade. +pub(crate) use self::postgres::PostgresStoreOptions; +/// Crate-internal storage symbol `PostgresTableDiagnosticSpec` shared through the crate-root facade. +pub(crate) use self::postgres::PostgresTableDiagnosticSpec; +/// Crate-internal storage symbol `RAW_TRANSACTIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::RAW_TRANSACTIONS_TABLE_NAME; +/// Crate-internal storage symbol `STORE_SCHEMA_ADVISORY_LOCK_ID` shared through the crate-root facade. +pub(crate) use self::postgres::STORE_SCHEMA_ADVISORY_LOCK_ID; +/// Crate-internal storage symbol `TRANSACTION_OBSERVATIONS_TABLE_NAME` shared through the crate-root facade. +pub(crate) use self::postgres::TRANSACTION_OBSERVATIONS_TABLE_NAME; +/// Crate-internal storage symbol `apply_core_store_schema` shared through the crate-root facade. +pub(crate) use self::postgres::apply_core_store_schema; +/// Crate-internal storage symbol `apply_decode_store_schema` shared through the crate-root facade. +pub(crate) use self::postgres::apply_decode_store_schema; +/// Crate-internal storage symbol `apply_raw_store_schema` shared through the crate-root facade. +pub(crate) use self::postgres::apply_raw_store_schema; +/// Crate-internal storage symbol `core_store_schema_statements` shared through the crate-root facade. +pub(crate) use self::postgres::core_store_schema_statements; +/// Crate-internal storage symbol `core_store_table_diagnostic_specs` shared through the crate-root facade. +pub(crate) use self::postgres::core_store_table_diagnostic_specs; +/// Crate-internal storage symbol `decode_store_schema_statements` shared through the crate-root facade. +pub(crate) use self::postgres::decode_store_schema_statements; +/// Crate-internal storage symbol `decode_store_table_diagnostic_specs` shared through the crate-root facade. +pub(crate) use self::postgres::decode_store_table_diagnostic_specs; +/// Crate-internal storage symbol `has_raw_transaction_signature` shared through the crate-root facade. +pub(crate) use self::postgres::has_raw_transaction_signature; +/// Crate-internal storage symbol `has_transaction_observation_key` shared through the crate-root facade. +pub(crate) use self::postgres::has_transaction_observation_key; +/// Crate-internal storage symbol `insert_core_account_keys` shared through the crate-root facade. +pub(crate) use self::postgres::insert_core_account_keys; +/// Crate-internal storage symbol `insert_core_balance_changes` shared through the crate-root facade. +pub(crate) use self::postgres::insert_core_balance_changes; +/// Crate-internal storage symbol `insert_core_inner_instructions` shared through the crate-root facade. +pub(crate) use self::postgres::insert_core_inner_instructions; +/// Crate-internal storage symbol `insert_core_instructions` shared through the crate-root facade. +pub(crate) use self::postgres::insert_core_instructions; +/// Crate-internal storage symbol `insert_core_logs` shared through the crate-root facade. +pub(crate) use self::postgres::insert_core_logs; +/// Crate-internal storage symbol `insert_core_transaction` shared through the crate-root facade. +pub(crate) use self::postgres::insert_core_transaction; +/// Crate-internal storage symbol `insert_raw_transaction` shared through the crate-root facade. +pub(crate) use self::postgres::insert_raw_transaction; +/// Crate-internal storage symbol `insert_transaction_observation` shared through the crate-root facade. +pub(crate) use self::postgres::insert_transaction_observation; +/// Crate-internal storage symbol `is_core_extraction_current` shared through the crate-root facade. +pub(crate) use self::postgres::is_core_extraction_current; +/// Crate-internal storage symbol `is_decode_current` shared through the crate-root facade. +pub(crate) use self::postgres::is_decode_current; +/// Crate-internal storage symbol `list_core_instruction_replay_inputs` shared through the crate-root facade. +pub(crate) use self::postgres::list_core_instruction_replay_inputs; +/// Crate-internal storage symbol `list_core_instructions_for_replay` shared through the crate-root facade. +pub(crate) use self::postgres::list_core_instructions_for_replay; +/// Crate-internal storage symbol `list_decode_coverage_summary` shared through the crate-root facade. +pub(crate) use self::postgres::list_decode_coverage_summary; +/// Crate-internal storage symbol `list_decode_inputs` shared through the crate-root facade. +pub(crate) use self::postgres::list_decode_inputs; +/// Crate-internal storage symbol `list_decode_replay_inputs` shared through the crate-root facade. +pub(crate) use self::postgres::list_decode_replay_inputs; +/// Crate-internal storage symbol `list_materialized_events` shared through the crate-root facade. +pub(crate) use self::postgres::list_materialized_events; +/// Crate-internal storage symbol `list_raw_transactions_for_core_extraction` shared through the crate-root facade. +pub(crate) use self::postgres::list_raw_transactions_for_core_extraction; +/// Crate-internal storage symbol `list_replay_entity_summaries` shared through the crate-root facade. +pub(crate) use self::postgres::list_replay_entity_summaries; +/// Crate-internal storage symbol `list_replay_program_summaries` shared through the crate-root facade. +pub(crate) use self::postgres::list_replay_program_summaries; +/// Crate-internal storage symbol `list_replay_transaction_candidates` shared through the crate-root facade. +pub(crate) use self::postgres::list_replay_transaction_candidates; +/// Crate-internal storage symbol `load_current_schema` shared through the crate-root facade. +pub(crate) use self::postgres::load_current_schema; +/// Crate-internal storage symbol `load_latest_migration_version` shared through the crate-root facade. +pub(crate) use self::postgres::load_latest_migration_version; +/// Crate-internal storage symbol `load_migration_table_name` shared through the crate-root facade. +pub(crate) use self::postgres::load_migration_table_name; +/// Crate-internal storage symbol `load_server_version` shared through the crate-root facade. +pub(crate) use self::postgres::load_server_version; +/// Crate-internal storage symbol `load_table_statistics` shared through the crate-root facade. +pub(crate) use self::postgres::load_table_statistics; +/// Crate-internal storage symbol `mark_core_extraction_failed` shared through the crate-root facade. +pub(crate) use self::postgres::mark_core_extraction_failed; +/// Crate-internal storage symbol `mark_decode_failed` shared through the crate-root facade. +pub(crate) use self::postgres::mark_decode_failed; +/// Crate-internal storage symbol `persist_core_extraction` shared through the crate-root facade. +pub(crate) use self::postgres::persist_core_extraction; +/// Crate-internal storage symbol `persist_decode_coverage_declarations` shared through the crate-root facade. +pub(crate) use self::postgres::persist_decode_coverage_declarations; +/// Crate-internal storage symbol `persist_decode_result` shared through the crate-root facade. +pub(crate) use self::postgres::persist_decode_result; +/// Crate-internal storage symbol `persist_materialization_result` shared through the crate-root facade. +pub(crate) use self::postgres::persist_materialization_result; +#[cfg(test)] +/// Crate-internal serialized PostgreSQL test guard shared through the crate-root facade. +pub(crate) use self::postgres::postgres_test_guard; +/// Crate-internal storage symbol `raw_store_schema_statements` shared through the crate-root facade. +pub(crate) use self::postgres::raw_store_schema_statements; +/// Crate-internal storage symbol `raw_store_table_diagnostic_specs` shared through the crate-root facade. +pub(crate) use self::postgres::raw_store_table_diagnostic_specs; +/// Crate-internal storage symbol `run_health_check` shared through the crate-root facade. +pub(crate) use self::postgres::run_health_check; +/// Crate-internal storage symbol `table_exists` shared through the crate-root facade. +pub(crate) use self::postgres::table_exists; +/// Crate-internal storage symbol `table_stats_kb_sol_core_account_keys_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_core_account_keys_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_core_balance_changes_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_core_balance_changes_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_core_inner_instructions_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_core_inner_instructions_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_core_instructions_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_core_instructions_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_core_logs_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_core_logs_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_core_transactions_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_core_transactions_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_decode_coverage_declarations_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_decode_coverage_declarations_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_decode_coverage_observations_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_decode_coverage_observations_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_decode_events_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_decode_events_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_mat_events_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_mat_events_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_obs_transaction_observations_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_obs_transaction_observations_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_ops_processing_ledger_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_ops_processing_ledger_sql; +/// Crate-internal storage symbol `table_stats_kb_sol_raw_transactions_sql` shared through the crate-root facade. +pub(crate) use self::postgres::table_stats_kb_sol_raw_transactions_sql; +/// Crate-internal storage symbol `update_core_instruction_lifecycle` shared through the crate-root facade. +pub(crate) use self::postgres::update_core_instruction_lifecycle; +/// Crate-internal storage symbol `update_raw_payload_lifecycle` shared through the crate-root facade. +pub(crate) use self::postgres::update_raw_payload_lifecycle; +/// Crate-internal storage symbol `validate_core_store_table_names` shared through the crate-root facade. +pub(crate) use self::postgres::validate_core_store_table_names; +/// Crate-internal storage symbol `validate_decode_store_table_names` shared through the crate-root facade. +pub(crate) use self::postgres::validate_decode_store_table_names; +/// Crate-internal storage symbol `validate_raw_store_table_names` shared through the crate-root facade. +pub(crate) use self::postgres::validate_raw_store_table_names; +/// Transitional logical schema contract identifier used until the `0.5.3-pre.3` rebuild. +pub use self::constants::STORE_SCHEMA_CONTRACT_VERSION; /// Core account key insert contract. pub use self::contracts::CoreAccountKeyInsert; -/// Core account key SQL-like row contract. +/// Core account key persisted row contract. pub use self::contracts::CoreAccountKeyRow; /// Core account key source category. pub use self::contracts::CoreAccountKeySource; @@ -23,19 +187,19 @@ pub use self::contracts::CoreAccountKeySource; pub use self::contracts::CoreBalanceChangeInsert; /// Core balance change kind. pub use self::contracts::CoreBalanceChangeKind; -/// Core balance change SQL-like row contract. +/// Core balance change persisted row contract. pub use self::contracts::CoreBalanceChangeRow; -/// Complete normalized core extraction write bundle. +/// Complete normalized Core extraction write bundle. pub use self::contracts::CoreExtractionBundle; -/// Failure details persisted for one canonical to core extraction attempt. +/// Failure details persisted for one canonical-to-Core extraction attempt. pub use self::contracts::CoreExtractionFailure; -/// Bounded canonical transaction selection filter for core extraction. +/// Bounded canonical transaction selection filter for Core extraction. pub use self::contracts::CoreExtractionSelectionFilter; -/// Canonical transaction to core extraction storage behavior. +/// Canonical transaction-to-Core extraction storage behavior. pub use self::contracts::CoreExtractionStore; /// Core inner instruction insert contract. pub use self::contracts::CoreInnerInstructionInsert; -/// Core inner instruction SQL-like row contract. +/// Core inner instruction persisted row contract. pub use self::contracts::CoreInnerInstructionRow; /// Core instruction insert contract. pub use self::contracts::CoreInstructionInsert; @@ -45,15 +209,15 @@ pub use self::contracts::CoreInstructionLifecycleMark; pub use self::contracts::CoreInstructionProcessingState; /// Core instruction replay filter contract. pub use self::contracts::CoreInstructionReplayFilter; -/// Core instruction SQL-like row contract. +/// Core instruction persisted row contract. pub use self::contracts::CoreInstructionRow; /// Core log insert contract. pub use self::contracts::CoreLogInsert; -/// Core log SQL-like row contract. +/// Core log persisted row contract. pub use self::contracts::CoreLogRow; /// Core transaction insert contract. pub use self::contracts::CoreTransactionInsert; -/// Core transaction SQL-like row contract. +/// Core transaction persisted row contract. pub use self::contracts::CoreTransactionRow; /// Core Solana storage behavior. pub use self::contracts::CoreTransactionStore; @@ -65,7 +229,7 @@ pub use self::contracts::DecodeCoverageDeclarationInsert; pub use self::contracts::DecodeCoverageObservationInsert; /// One row of aggregated decoder coverage diagnostics. pub use self::contracts::DecodeCoverageSummaryRow; -/// Failed decode attempt persisted in the common ledger. +/// Failed decode attempt persisted in the common processing ledger. pub use self::contracts::DecodeFailure; /// One processor-owned decoded observation row. pub use self::contracts::DecodeObservationInsert; @@ -75,46 +239,28 @@ pub use self::contracts::DecodePersistenceBundle; pub use self::contracts::DecodePipelineStore; /// Bounded contextual instruction selection filter for decode campaigns. pub use self::contracts::DecodeSelectionFilter; -/// Decoded event insert contract. -pub use self::contracts::DecodedEventInsert; -/// Decoded event SQL-like row contract. -pub use self::contracts::DecodedEventRow; -/// Decoded event storage behavior. -pub use self::contracts::DecodedEventStore; /// Insert or upsert result contract returned by repositories. pub use self::contracts::InsertOutcome; /// Maximum number of materialized rows returned by one bounded query. pub use self::contracts::MAX_MATERIALIZED_EVENT_QUERY_ROWS; /// Maximum repository page size. pub use self::contracts::MAX_PAGE_SIZE; +/// Maximum number of rows returned by one replay candidate query. +pub use self::contracts::MAX_REPLAY_CANDIDATE_ROWS; /// Atomic persistence bundle for one materializer and one decoded observation. pub use self::contracts::MaterializationPersistenceBundle; /// Bounded read-only materialized event selection. pub use self::contracts::MaterializedEventFilter; -/// Materialized event insert contract. -pub use self::contracts::MaterializedEventInsert; /// One materialized output returned by a bounded query. pub use self::contracts::MaterializedEventQueryRow; -/// Materialized event SQL-like row contract. -pub use self::contracts::MaterializedEventRow; -/// Materialized event storage behavior. -pub use self::contracts::MaterializedEventStore; -/// One processor-owned materialized output row. +/// One processor-owned generic materialized output row. pub use self::contracts::MaterializedOutputInsert; /// Page request contract for repository list operations. pub use self::contracts::PageRequest; /// Stable processing ledger identity. pub use self::contracts::ProcessingLedgerIdentity; -/// Processing ledger mark request contract. -pub use self::contracts::ProcessingLedgerMark; -/// Processing ledger SQL-like row contract. -pub use self::contracts::ProcessingLedgerRow; /// Stable processing ledger status. pub use self::contracts::ProcessingLedgerStatus; -/// Processing ledger storage behavior. -pub use self::contracts::ProcessingLedgerStore; -/// Program observation storage behavior. -pub use self::contracts::ProgramObservationStore; /// Raw payload lifecycle mark request. pub use self::contracts::RawPayloadLifecycleMark; /// Raw payload processing state. @@ -123,135 +269,75 @@ pub use self::contracts::RawPayloadProcessingState; pub use self::contracts::RawPayloadRetentionState; /// Canonical raw Solana transaction insert contract. pub use self::contracts::RawTransactionInsert; -/// Canonical raw Solana transaction SQL-like row contract. +/// Canonical raw Solana transaction persisted row contract. pub use self::contracts::RawTransactionRow; /// Raw transaction storage behavior. pub use self::contracts::RawTransactionStore; +/// Bounded read-only filter for Core entity summaries. +pub use self::contracts::ReplayEntityFilter; +/// Core entity kind used while filtering replay candidates. +pub use self::contracts::ReplayEntityKind; +/// Aggregated mint, owner, or account-key occurrences from Core facts. +pub use self::contracts::ReplayEntitySummary; +/// Bounded read-only filter for program summaries. +pub use self::contracts::ReplayProgramFilter; +/// Program occurrence scope used while filtering replay candidates. +pub use self::contracts::ReplayProgramScope; +/// Aggregated program occurrences across top-level instructions, inner instructions, and logs. +pub use self::contracts::ReplayProgramSummary; +/// One raw transaction candidate enriched with Core and processing diagnostics. +pub use self::contracts::ReplayTransactionCandidate; +/// Bounded read-only filter for transaction replay candidates. +pub use self::contracts::ReplayTransactionFilter; /// Sort direction for repository list operations. pub use self::contracts::SortDirection; -/// Store backend diagnostic contract. +/// Store backend diagnostic descriptor. pub use self::contracts::StoreBackendDescriptor; -/// Store backend kind contract. -pub use self::contracts::StoreBackendKind; +/// Backend-neutral diagnostic snapshot. +pub use self::contracts::StoreBackendDiagnostics; +/// Store configuration summary safe for UI and logs. +pub use self::contracts::StoreConfigurationSummary; /// Store backend health snapshot. pub use self::contracts::StoreHealthSnapshot; /// Store backend health status. pub use self::contracts::StoreHealthStatus; /// Store health storage behavior. pub use self::contracts::StoreHealthStore; +/// Store initialization status. +pub use self::contracts::StoreInitializationStatus; +/// Initialization and model-verification summary captured while opening a store. +pub use self::contracts::StoreInitializationSummary; /// Store migration diagnostic snapshot contract. pub use self::contracts::StoreMigrationSnapshot; /// Store migration status contract. pub use self::contracts::StoreMigrationStatus; +/// Store model verification status. +pub use self::contracts::StoreModelVerificationStatus; +/// Verification summary for one logical store model. +pub use self::contracts::StoreModelVerificationSummary; +/// Count summary for one backend object category without exposing physical names. +pub use self::contracts::StoreObjectVerificationSummary; +/// Read-only diagnostics for one logical store resource. +pub use self::contracts::StoreResourceDiagnostics; +/// Read-only statistics for one logical store resource. +pub use self::contracts::StoreResourceStatistics; +/// Complete runtime summary safe for application diagnostics. +pub use self::contracts::StoreRuntimeSummary; /// Transaction acquisition observation insert contract. pub use self::contracts::TransactionObservationInsert; /// Transaction acquisition observation origin. pub use self::contracts::TransactionObservationOrigin; -/// Transaction acquisition observation SQL-like row contract. +/// Transaction acquisition observation persisted row contract. pub use self::contracts::TransactionObservationRow; /// Transaction acquisition observation status. pub use self::contracts::TransactionObservationStatus; /// Storage error helper functions. pub use self::contracts::storage_contract_error; -/// Allowed Solana table domains encoded in table names. -pub use self::postgres::ALLOWED_SOLANA_TABLE_DOMAINS; -/// Core account key table name. -pub use self::postgres::CORE_ACCOUNT_KEYS_TABLE_NAME; -/// Core balance change table name. -pub use self::postgres::CORE_BALANCE_CHANGES_TABLE_NAME; -/// Core inner instruction table name. -pub use self::postgres::CORE_INNER_INSTRUCTIONS_TABLE_NAME; -/// Core instruction table name. -pub use self::postgres::CORE_INSTRUCTIONS_TABLE_NAME; -/// Core log table name. -pub use self::postgres::CORE_LOGS_TABLE_NAME; -/// Core Solana table names. -pub use self::postgres::CORE_STORE_TABLE_NAMES; -/// Core transaction table name. -pub use self::postgres::CORE_TRANSACTIONS_TABLE_NAME; -/// Machine-readable decoder coverage declaration table name. -pub use self::postgres::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME; -/// Observed decoder coverage classification table name. -pub use self::postgres::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME; -/// Versioned decoded event table name. -pub use self::postgres::DECODE_EVENTS_TABLE_NAME; -/// Decode and materialization table names. -pub use self::postgres::DECODE_STORE_TABLE_NAMES; -/// Default PostgreSQL schema policy. -pub use self::postgres::DEFAULT_SCHEMA_POLICY; -/// Versioned materialized output table name. -pub use self::postgres::MATERIALIZED_EVENTS_TABLE_NAME; -/// Maximum number of rows returned by one replay candidate query. -pub use self::postgres::MAX_REPLAY_CANDIDATE_ROWS; -/// PostgreSQL migration strategy used by this crate. -pub use self::postgres::MIGRATION_STRATEGY; -/// Migration table name used by sqlx. -pub use self::postgres::MIGRATION_TABLE_NAME; -/// Processing ledger table name. -pub use self::postgres::PROCESSING_LEDGER_TABLE_NAME; -/// PostgreSQL diagnostic snapshot. -pub use self::postgres::PostgresBackendDiagnostics; -/// Bounded read-only filter for core entity summaries. -pub use self::postgres::PostgresReplayEntityFilter; -/// Core entity kind used while filtering replay candidates. -pub use self::postgres::PostgresReplayEntityKind; -/// Aggregated mint, owner or account-key occurrences from core tables. -pub use self::postgres::PostgresReplayEntitySummary; -/// Bounded read-only filter for program summaries. -pub use self::postgres::PostgresReplayProgramFilter; -/// Program occurrence scope used while filtering replay candidates. -pub use self::postgres::PostgresReplayProgramScope; -/// Aggregated program occurrences across outer, inner and linked logs. -pub use self::postgres::PostgresReplayProgramSummary; -/// One raw transaction candidate enriched with core and ledger diagnostics. -pub use self::postgres::PostgresReplayTransactionCandidate; -/// Bounded read-only filter for transaction replay candidates. -pub use self::postgres::PostgresReplayTransactionFilter; -/// PostgreSQL store handle. -pub use self::postgres::PostgresStore; -/// PostgreSQL store connection options. -pub use self::postgres::PostgresStoreOptions; -/// PostgreSQL diagnostic table specification. -pub use self::postgres::PostgresTableDiagnosticSpec; -/// PostgreSQL table diagnostic snapshot. -pub use self::postgres::PostgresTableDiagnostics; -/// PostgreSQL table statistics snapshot. -pub use self::postgres::PostgresTableStatistics; -/// Canonical transaction acquisition table names. -pub use self::postgres::RAW_STORE_TABLE_NAMES; -/// Canonical raw transaction table name. -pub use self::postgres::RAW_TRANSACTIONS_TABLE_NAME; -/// Solana application table prefix. -pub use self::postgres::SOLANA_TABLE_PREFIX; -/// Advisory lock id used while initializing store tables. -pub use self::postgres::STORE_SCHEMA_ADVISORY_LOCK_ID; -/// Transaction acquisition observation table name. -pub use self::postgres::TRANSACTION_OBSERVATIONS_TABLE_NAME; -/// Core store SQL statements applied by the idempotent initializer. -pub use self::postgres::core_store_schema_statements; -/// Core table diagnostic specs. -pub use self::postgres::core_store_table_diagnostic_specs; -/// Decode and materialization SQL statements applied by the idempotent initializer. -pub use self::postgres::decode_store_schema_statements; -/// Decode and materialization table diagnostic specs. -pub use self::postgres::decode_store_table_diagnostic_specs; -/// Returns true when a Solana table name follows canonical rules. -pub use self::postgres::is_valid_solana_table_name; -/// Returns a DSN masked for logs and UI diagnostics. -pub use self::postgres::mask_postgres_dsn; -/// Canonical transaction acquisition SQL statements. -pub use self::postgres::raw_store_schema_statements; -/// Canonical transaction acquisition diagnostic specs. -pub use self::postgres::raw_store_table_diagnostic_specs; -/// Validates core store table names. -pub use self::postgres::validate_core_store_table_names; -/// Validates decode and materialization store table names. -pub use self::postgres::validate_decode_store_table_names; -/// Validates canonical transaction acquisition table names. -pub use self::postgres::validate_raw_store_table_names; -/// Validates a canonical Solana table name. -pub use self::postgres::validate_solana_table_name; -/// Current normalized core replay input contract version. +/// Backend-agnostic persistent store facade. +pub use self::store::Store; +/// Backend-agnostic options used to open one persistent store. +pub use self::store::StoreOpenOptions; +/// Current normalized Core replay input contract version. pub use ks_lib::MD_CORE_REPLAY_INPUT_CONTRACT_VERSION; /// Decoder replay input containing one instruction plus extracted transaction context. pub use ks_lib::MdCoreInstructionReplayInput; diff --git a/ks-store/src/postgres.rs b/ks-store/src/postgres.rs index 7abe144..117eca7 100644 --- a/ks-store/src/postgres.rs +++ b/ks-store/src/postgres.rs @@ -1,111 +1,94 @@ // file: ks-store/src/postgres.rs -// version: 4 +// version: 11 -//! PostgreSQL storage implementation boundary. +//! Private PostgreSQL implementation of the backend-agnostic store contracts. mod migrations; mod query; -mod replay_candidates; mod repository; mod store; #[cfg(test)] mod test_serial; -/// Allowed Solana table domains encoded in table names. -pub use self::migrations::ALLOWED_SOLANA_TABLE_DOMAINS; -/// Core account key table name. -pub use self::migrations::CORE_ACCOUNT_KEYS_TABLE_NAME; -/// Core balance change table name. -pub use self::migrations::CORE_BALANCE_CHANGES_TABLE_NAME; -/// Core inner instruction table name. -pub use self::migrations::CORE_INNER_INSTRUCTIONS_TABLE_NAME; -/// Core instruction table name. -pub use self::migrations::CORE_INSTRUCTIONS_TABLE_NAME; -/// Core log table name. -pub use self::migrations::CORE_LOGS_TABLE_NAME; -/// Core Solana table names introduced by `0.2.4`. -pub use self::migrations::CORE_STORE_TABLE_NAMES; -/// Core transaction table name. -pub use self::migrations::CORE_TRANSACTIONS_TABLE_NAME; -/// Machine-readable decoder coverage declaration table name. -pub use self::migrations::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME; -/// Observed decoder coverage classification table name. -pub use self::migrations::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME; -/// Versioned decoded event table name. -pub use self::migrations::DECODE_EVENTS_TABLE_NAME; -/// Decode and materialization table names introduced by `0.4.0`. -pub use self::migrations::DECODE_STORE_TABLE_NAMES; -/// Default PostgreSQL schema policy. -pub use self::migrations::DEFAULT_SCHEMA_POLICY; -/// Versioned materialized output table name. -pub use self::migrations::MATERIALIZED_EVENTS_TABLE_NAME; -/// PostgreSQL migration strategy used by this crate. -pub use self::migrations::MIGRATION_STRATEGY; -/// Migration table name used by sqlx when migrations are enabled later. -pub use self::migrations::MIGRATION_TABLE_NAME; -/// Processing ledger table name. -pub use self::migrations::PROCESSING_LEDGER_TABLE_NAME; -/// Table diagnostic metadata. -pub use self::migrations::PostgresTableDiagnosticSpec; -/// Canonical transaction acquisition table names active since `0.3.1`. -pub use self::migrations::RAW_STORE_TABLE_NAMES; -/// Canonical raw transaction table name. -pub use self::migrations::RAW_TRANSACTIONS_TABLE_NAME; -/// Solana application table prefix. -pub use self::migrations::SOLANA_TABLE_PREFIX; -/// Advisory lock id used while initializing store schemas. -pub use self::migrations::STORE_SCHEMA_ADVISORY_LOCK_ID; -/// Transaction acquisition observation table name. -pub use self::migrations::TRANSACTION_OBSERVATIONS_TABLE_NAME; -/// Core store SQL statements applied by the idempotent initializer. -pub use self::migrations::core_store_schema_statements; -/// Core table diagnostic specs. -pub use self::migrations::core_store_table_diagnostic_specs; -/// Decode and materialization SQL statements applied by the idempotent initializer. -pub use self::migrations::decode_store_schema_statements; -/// Decode and materialization table diagnostic specs. -pub use self::migrations::decode_store_table_diagnostic_specs; -/// Returns true when a Solana table name follows the canonical prefix and domain rules. -pub use self::migrations::is_valid_solana_table_name; -/// Canonical transaction acquisition SQL statements applied by the initializer. -pub use self::migrations::raw_store_schema_statements; -/// Canonical transaction acquisition diagnostic specs. -pub use self::migrations::raw_store_table_diagnostic_specs; -/// Validates core store table names. -pub use self::migrations::validate_core_store_table_names; -/// Validates decode and materialization store table names. -pub use self::migrations::validate_decode_store_table_names; -/// Validates canonical transaction acquisition table names. -pub use self::migrations::validate_raw_store_table_names; -/// Validates a canonical Solana table name. -pub use self::migrations::validate_solana_table_name; -/// Maximum number of rows returned by one replay candidate query. -pub use self::replay_candidates::MAX_REPLAY_CANDIDATE_ROWS; -/// Bounded read-only filter for core entity summaries. -pub use self::replay_candidates::PostgresReplayEntityFilter; -/// Core entity kind used while filtering replay candidates. -pub use self::replay_candidates::PostgresReplayEntityKind; -/// Aggregated mint, owner or account-key occurrences from core tables. -pub use self::replay_candidates::PostgresReplayEntitySummary; -/// Bounded read-only filter for program summaries. -pub use self::replay_candidates::PostgresReplayProgramFilter; -/// Program occurrence scope used while filtering replay candidates. -pub use self::replay_candidates::PostgresReplayProgramScope; -/// Aggregated program occurrences across outer, inner and linked logs. -pub use self::replay_candidates::PostgresReplayProgramSummary; -/// One raw transaction candidate enriched with core and ledger diagnostics. -pub use self::replay_candidates::PostgresReplayTransactionCandidate; -/// Bounded read-only filter for transaction replay candidates. -pub use self::replay_candidates::PostgresReplayTransactionFilter; -/// PostgreSQL diagnostic snapshot. -pub use self::store::PostgresBackendDiagnostics; -/// Minimal PostgreSQL store handle. -pub use self::store::PostgresStore; -/// PostgreSQL store connection options. -pub use self::store::PostgresStoreOptions; -/// PostgreSQL table diagnostic snapshot. -pub use self::store::PostgresTableDiagnostics; -/// PostgreSQL table statistics snapshot. -pub use self::store::PostgresTableStatistics; -/// Returns a DSN masked for logs and UI diagnostics. -pub use self::store::mask_postgres_dsn; +pub(crate) use self::migrations::CORE_ACCOUNT_KEYS_TABLE_NAME; +pub(crate) use self::migrations::CORE_BALANCE_CHANGES_TABLE_NAME; +pub(crate) use self::migrations::CORE_INNER_INSTRUCTIONS_TABLE_NAME; +pub(crate) use self::migrations::CORE_INSTRUCTIONS_TABLE_NAME; +pub(crate) use self::migrations::CORE_LOGS_TABLE_NAME; +pub(crate) use self::migrations::CORE_TRANSACTIONS_TABLE_NAME; +pub(crate) use self::migrations::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME; +pub(crate) use self::migrations::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME; +pub(crate) use self::migrations::DECODE_EVENTS_TABLE_NAME; +pub(crate) use self::migrations::MATERIALIZED_EVENTS_TABLE_NAME; +pub(crate) use self::migrations::PROCESSING_LEDGER_TABLE_NAME; +pub(crate) use self::migrations::PostgresTableDiagnosticSpec; +pub(crate) use self::migrations::RAW_TRANSACTIONS_TABLE_NAME; +pub(crate) use self::migrations::STORE_SCHEMA_ADVISORY_LOCK_ID; +pub(crate) use self::migrations::TRANSACTION_OBSERVATIONS_TABLE_NAME; +pub(crate) use self::migrations::core_store_schema_statements; +pub(crate) use self::migrations::core_store_table_diagnostic_specs; +pub(crate) use self::migrations::decode_store_schema_statements; +pub(crate) use self::migrations::decode_store_table_diagnostic_specs; +pub(crate) use self::migrations::raw_store_schema_statements; +pub(crate) use self::migrations::raw_store_table_diagnostic_specs; +pub(crate) use self::migrations::table_stats_kb_sol_core_account_keys_sql; +pub(crate) use self::migrations::table_stats_kb_sol_core_balance_changes_sql; +pub(crate) use self::migrations::table_stats_kb_sol_core_inner_instructions_sql; +pub(crate) use self::migrations::table_stats_kb_sol_core_instructions_sql; +pub(crate) use self::migrations::table_stats_kb_sol_core_logs_sql; +pub(crate) use self::migrations::table_stats_kb_sol_core_transactions_sql; +pub(crate) use self::migrations::table_stats_kb_sol_decode_coverage_declarations_sql; +pub(crate) use self::migrations::table_stats_kb_sol_decode_coverage_observations_sql; +pub(crate) use self::migrations::table_stats_kb_sol_decode_events_sql; +pub(crate) use self::migrations::table_stats_kb_sol_mat_events_sql; +pub(crate) use self::migrations::table_stats_kb_sol_obs_transaction_observations_sql; +pub(crate) use self::migrations::table_stats_kb_sol_ops_processing_ledger_sql; +pub(crate) use self::migrations::table_stats_kb_sol_raw_transactions_sql; +pub(crate) use self::migrations::validate_core_store_table_names; +pub(crate) use self::migrations::validate_decode_store_table_names; +pub(crate) use self::migrations::validate_raw_store_table_names; +pub(crate) use self::query::apply_core_store_schema; +pub(crate) use self::query::apply_decode_store_schema; +pub(crate) use self::query::apply_raw_store_schema; +pub(crate) use self::query::has_raw_transaction_signature; +pub(crate) use self::query::has_transaction_observation_key; +pub(crate) use self::query::insert_core_account_keys; +pub(crate) use self::query::insert_core_balance_changes; +pub(crate) use self::query::insert_core_inner_instructions; +pub(crate) use self::query::insert_core_instructions; +pub(crate) use self::query::insert_core_logs; +pub(crate) use self::query::insert_core_transaction; +pub(crate) use self::query::insert_raw_transaction; +pub(crate) use self::query::insert_transaction_observation; +pub(crate) use self::query::is_core_extraction_current; +pub(crate) use self::query::is_decode_current; +pub(crate) use self::query::list_core_instruction_replay_inputs; +pub(crate) use self::query::list_core_instructions_for_replay; +pub(crate) use self::query::list_decode_coverage_summary; +pub(crate) use self::query::list_decode_inputs; +pub(crate) use self::query::list_decode_replay_inputs; +pub(crate) use self::query::list_materialized_events; +pub(crate) use self::query::list_raw_transactions_for_core_extraction; +pub(crate) use self::query::list_replay_entity_summaries; +pub(crate) use self::query::list_replay_program_summaries; +pub(crate) use self::query::list_replay_transaction_candidates; +pub(crate) use self::query::load_current_schema; +pub(crate) use self::query::load_latest_migration_version; +pub(crate) use self::query::load_migration_table_name; +pub(crate) use self::query::load_server_version; +pub(crate) use self::query::load_table_statistics; +pub(crate) use self::query::mark_core_extraction_failed; +pub(crate) use self::query::mark_decode_failed; +pub(crate) use self::query::persist_core_extraction; +pub(crate) use self::query::persist_decode_coverage_declarations; +pub(crate) use self::query::persist_decode_result; +pub(crate) use self::query::persist_materialization_result; +pub(crate) use self::query::run_health_check; +pub(crate) use self::query::table_exists; +pub(crate) use self::query::update_core_instruction_lifecycle; +pub(crate) use self::query::update_raw_payload_lifecycle; +pub(crate) use self::store::PostgresStore; +pub(crate) use self::store::PostgresStoreOptions; + +#[cfg(test)] +pub(crate) use self::test_serial::postgres_test_guard; diff --git a/ks-store/src/postgres/migrations.rs b/ks-store/src/postgres/migrations.rs index 54ec2b7..31b0c28 100644 --- a/ks-store/src/postgres/migrations.rs +++ b/ks-store/src/postgres/migrations.rs @@ -1,165 +1,175 @@ // file: ks-store/src/postgres/migrations.rs -// version: 3 +// version: 7 //! PostgreSQL migration conventions for the storage backend. -/// PostgreSQL schema policy used by application migrations. -pub const DEFAULT_SCHEMA_POLICY: &str = "current_profile_schema"; /// Canonical Solana table prefix. -pub const SOLANA_TABLE_PREFIX: &str = "kb_sol_"; -/// Migration table name used by sqlx when migrations are enabled later. -pub const MIGRATION_TABLE_NAME: &str = "_sqlx_migrations"; -/// Migration strategy used by the canonical transaction and core stores. -pub const MIGRATION_STRATEGY: &str = - "idempotent_canonical_transaction_observation_and_core_tables_without_application_schemas"; +const SOLANA_TABLE_PREFIX: &str = "kb_sol_"; /// Advisory lock id used while applying idempotent store schema statements. -pub const STORE_SCHEMA_ADVISORY_LOCK_ID: i64 = 2_024_000_204; +pub(crate) const STORE_SCHEMA_ADVISORY_LOCK_ID: i64 = 2_024_000_204; /// Canonical raw transaction table name. -pub const RAW_TRANSACTIONS_TABLE_NAME: &str = "kb_sol_raw_transactions"; +pub(crate) const RAW_TRANSACTIONS_TABLE_NAME: &str = "kb_sol_raw_transactions"; /// Lightweight transaction acquisition observation table name. -pub const TRANSACTION_OBSERVATIONS_TABLE_NAME: &str = "kb_sol_obs_transaction_observations"; +pub(crate) const TRANSACTION_OBSERVATIONS_TABLE_NAME: &str = "kb_sol_obs_transaction_observations"; /// Core transaction table name. -pub const CORE_TRANSACTIONS_TABLE_NAME: &str = "kb_sol_core_transactions"; +pub(crate) const CORE_TRANSACTIONS_TABLE_NAME: &str = "kb_sol_core_transactions"; /// Core account key table name. -pub const CORE_ACCOUNT_KEYS_TABLE_NAME: &str = "kb_sol_core_account_keys"; +pub(crate) const CORE_ACCOUNT_KEYS_TABLE_NAME: &str = "kb_sol_core_account_keys"; /// Core instruction table name. -pub const CORE_INSTRUCTIONS_TABLE_NAME: &str = "kb_sol_core_instructions"; +pub(crate) const CORE_INSTRUCTIONS_TABLE_NAME: &str = "kb_sol_core_instructions"; /// Core inner instruction table name. -pub const CORE_INNER_INSTRUCTIONS_TABLE_NAME: &str = "kb_sol_core_inner_instructions"; +pub(crate) const CORE_INNER_INSTRUCTIONS_TABLE_NAME: &str = "kb_sol_core_inner_instructions"; /// Core log table name. -pub const CORE_LOGS_TABLE_NAME: &str = "kb_sol_core_logs"; +pub(crate) const CORE_LOGS_TABLE_NAME: &str = "kb_sol_core_logs"; /// Core balance change table name. -pub const CORE_BALANCE_CHANGES_TABLE_NAME: &str = "kb_sol_core_balance_changes"; +pub(crate) const CORE_BALANCE_CHANGES_TABLE_NAME: &str = "kb_sol_core_balance_changes"; /// Processing ledger table name. -pub const PROCESSING_LEDGER_TABLE_NAME: &str = "kb_sol_ops_processing_ledger"; +pub(crate) const PROCESSING_LEDGER_TABLE_NAME: &str = "kb_sol_ops_processing_ledger"; /// Versioned decoded event table name. -pub const DECODE_EVENTS_TABLE_NAME: &str = "kb_sol_decode_events"; +pub(crate) const DECODE_EVENTS_TABLE_NAME: &str = "kb_sol_decode_events"; /// Machine-readable decoder coverage declaration table name. -pub const DECODE_COVERAGE_DECLARATIONS_TABLE_NAME: &str = "kb_sol_decode_coverage_declarations"; +pub(crate) const DECODE_COVERAGE_DECLARATIONS_TABLE_NAME: &str = + "kb_sol_decode_coverage_declarations"; /// Observed decoder coverage classification table name. -pub const DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME: &str = "kb_sol_decode_coverage_observations"; +pub(crate) const DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME: &str = + "kb_sol_decode_coverage_observations"; /// Versioned materialized output table name. -pub const MATERIALIZED_EVENTS_TABLE_NAME: &str = "kb_sol_mat_events"; +pub(crate) const MATERIALIZED_EVENTS_TABLE_NAME: &str = "kb_sol_mat_events"; /// Canonical transaction acquisition table names active since `0.3.1`. -pub const RAW_STORE_TABLE_NAMES: &[&str] = - &[crate::RAW_TRANSACTIONS_TABLE_NAME, crate::TRANSACTION_OBSERVATIONS_TABLE_NAME]; +const RAW_STORE_TABLE_NAMES: &[&str] = + &[RAW_TRANSACTIONS_TABLE_NAME, TRANSACTION_OBSERVATIONS_TABLE_NAME]; /// Core Solana table names introduced by `0.2.4`. -pub const CORE_STORE_TABLE_NAMES: &[&str] = &[ - crate::CORE_TRANSACTIONS_TABLE_NAME, - crate::CORE_ACCOUNT_KEYS_TABLE_NAME, - crate::CORE_INSTRUCTIONS_TABLE_NAME, - crate::CORE_INNER_INSTRUCTIONS_TABLE_NAME, - crate::CORE_LOGS_TABLE_NAME, - crate::CORE_BALANCE_CHANGES_TABLE_NAME, - crate::PROCESSING_LEDGER_TABLE_NAME, +const CORE_STORE_TABLE_NAMES: &[&str] = &[ + CORE_TRANSACTIONS_TABLE_NAME, + CORE_ACCOUNT_KEYS_TABLE_NAME, + CORE_INSTRUCTIONS_TABLE_NAME, + CORE_INNER_INSTRUCTIONS_TABLE_NAME, + CORE_LOGS_TABLE_NAME, + CORE_BALANCE_CHANGES_TABLE_NAME, + PROCESSING_LEDGER_TABLE_NAME, ]; /// Decode and materialization table names introduced by `0.4.0`. -pub const DECODE_STORE_TABLE_NAMES: &[&str] = &[ - crate::DECODE_EVENTS_TABLE_NAME, - crate::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME, - crate::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME, - crate::MATERIALIZED_EVENTS_TABLE_NAME, +const DECODE_STORE_TABLE_NAMES: &[&str] = &[ + DECODE_EVENTS_TABLE_NAME, + DECODE_COVERAGE_DECLARATIONS_TABLE_NAME, + DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME, + MATERIALIZED_EVENTS_TABLE_NAME, ]; /// Allowed Solana table domains encoded after the `kb_sol_` prefix. -pub const ALLOWED_SOLANA_TABLE_DOMAINS: &[&str] = +const ALLOWED_SOLANA_TABLE_DOMAINS: &[&str] = &["raw", "core", "obs", "decode", "mat", "catalog", "agg", "ops", "wallet"]; /// Diagnostic metadata for one expected PostgreSQL table. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct PostgresTableDiagnosticSpec { - /// Expected table name. +pub(crate) struct PostgresTableDiagnosticSpec { + /// Physical PostgreSQL table name kept backend-private. pub table_name: &'static str, - /// Logical Solana domain encoded in the table name. - pub domain: &'static str, - /// Human-readable table role. + /// Stable backend-independent logical resource code. + pub resource_code: &'static str, + /// Logical store model code. + pub model_code: &'static str, + /// Human-readable resource role. pub role: &'static str, } /// Diagnostic metadata for canonical transaction acquisition tables. -pub fn raw_store_table_diagnostic_specs() -> [crate::PostgresTableDiagnosticSpec; 2] { +pub(crate) fn raw_store_table_diagnostic_specs() -> [crate::PostgresTableDiagnosticSpec; 2] { return [ crate::PostgresTableDiagnosticSpec { - table_name: crate::RAW_TRANSACTIONS_TABLE_NAME, - domain: "raw", + table_name: RAW_TRANSACTIONS_TABLE_NAME, + resource_code: "raw_transactions", + model_code: "raw", role: "Canonical transactions", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::TRANSACTION_OBSERVATIONS_TABLE_NAME, - domain: "obs", + table_name: TRANSACTION_OBSERVATIONS_TABLE_NAME, + resource_code: "transaction_observations", + model_code: "raw", role: "Transaction acquisition observations", }, ]; } /// Diagnostic metadata for core Solana store tables. -pub fn core_store_table_diagnostic_specs() -> [crate::PostgresTableDiagnosticSpec; 7] { +pub(crate) fn core_store_table_diagnostic_specs() -> [crate::PostgresTableDiagnosticSpec; 7] { return [ crate::PostgresTableDiagnosticSpec { - table_name: crate::CORE_TRANSACTIONS_TABLE_NAME, - domain: "core", + table_name: CORE_TRANSACTIONS_TABLE_NAME, + resource_code: "transactions", + model_code: "core", role: "Transactions", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::CORE_ACCOUNT_KEYS_TABLE_NAME, - domain: "core", + table_name: CORE_ACCOUNT_KEYS_TABLE_NAME, + resource_code: "account_keys", + model_code: "core", role: "Resolved account keys", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::CORE_INSTRUCTIONS_TABLE_NAME, - domain: "core", + table_name: CORE_INSTRUCTIONS_TABLE_NAME, + resource_code: "top_level_instructions", + model_code: "core", role: "Top-level instructions", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::CORE_INNER_INSTRUCTIONS_TABLE_NAME, - domain: "core", + table_name: CORE_INNER_INSTRUCTIONS_TABLE_NAME, + resource_code: "inner_instructions", + model_code: "core", role: "Inner instruction tree", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::CORE_LOGS_TABLE_NAME, - domain: "core", + table_name: CORE_LOGS_TABLE_NAME, + resource_code: "logs", + model_code: "core", role: "Transaction logs", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::CORE_BALANCE_CHANGES_TABLE_NAME, - domain: "core", + table_name: CORE_BALANCE_CHANGES_TABLE_NAME, + resource_code: "balance_changes", + model_code: "core", role: "Balance changes", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::PROCESSING_LEDGER_TABLE_NAME, - domain: "ops", + table_name: PROCESSING_LEDGER_TABLE_NAME, + resource_code: "processing_ledger", + model_code: "processing", role: "Processing ledger", }, ]; } /// Diagnostic metadata for decode and materialization store tables. -pub fn decode_store_table_diagnostic_specs() -> [crate::PostgresTableDiagnosticSpec; 4] { +pub(crate) fn decode_store_table_diagnostic_specs() -> [crate::PostgresTableDiagnosticSpec; 4] { return [ crate::PostgresTableDiagnosticSpec { - table_name: crate::DECODE_EVENTS_TABLE_NAME, - domain: "decode", + table_name: DECODE_EVENTS_TABLE_NAME, + resource_code: "decoded_observations", + model_code: "decode", role: "Versioned decoded observations", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME, - domain: "decode", + table_name: DECODE_COVERAGE_DECLARATIONS_TABLE_NAME, + resource_code: "decode_coverage_declarations", + model_code: "decode", role: "Declared decoder coverage", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME, - domain: "decode", + table_name: DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME, + resource_code: "decode_coverage_observations", + model_code: "decode", role: "Observed decoder coverage", }, crate::PostgresTableDiagnosticSpec { - table_name: crate::MATERIALIZED_EVENTS_TABLE_NAME, - domain: "mat", + table_name: MATERIALIZED_EVENTS_TABLE_NAME, + resource_code: "materialized_outputs", + model_code: "materialization", role: "Versioned materialized outputs", }, ]; } /// Idempotent SQL statements creating or upgrading the canonical transaction acquisition store. -pub fn raw_store_schema_statements() -> std::vec::Vec<&'static str> { +pub(crate) fn raw_store_schema_statements() -> std::vec::Vec<&'static str> { return vec![ crate::postgres::migrations::migrate_legacy_raw_table_name_sql(), crate::postgres::migrations::migrate_legacy_raw_columns_sql(), @@ -185,7 +195,7 @@ pub fn raw_store_schema_statements() -> std::vec::Vec<&'static str> { } /// Idempotent SQL statements creating the minimal core Solana store. -pub fn core_store_schema_statements() -> [&'static str; 28] { +pub(crate) fn core_store_schema_statements() -> [&'static str; 28] { return [ crate::postgres::migrations::create_table_kb_sol_core_transactions_sql(), crate::postgres::migrations::create_ux_kb_sol_core_transactions_signature_sql(), @@ -219,7 +229,7 @@ pub fn core_store_schema_statements() -> [&'static str; 28] { } /// Idempotent SQL statements creating the common decode and materialization store. -pub fn decode_store_schema_statements() -> [&'static str; 15] { +pub(crate) fn decode_store_schema_statements() -> [&'static str; 15] { return [ crate::postgres::migrations::create_table_kb_sol_decode_events_sql(), crate::postgres::migrations::create_ux_kb_sol_decode_events_identity_sql(), @@ -240,7 +250,7 @@ pub fn decode_store_schema_statements() -> [&'static str; 15] { } /// SQL conditionally renaming the historical raw RPC transaction table. -pub(in crate::postgres) fn migrate_legacy_raw_table_name_sql() -> &'static str { +fn migrate_legacy_raw_table_name_sql() -> &'static str { return r#"DO $$ BEGIN IF to_regclass('kb_sol_raw_rpc_transactions') IS NOT NULL @@ -255,7 +265,7 @@ $$"#; } /// SQL conditionally renaming historical raw payload columns and adding the format version. -pub(in crate::postgres) fn migrate_legacy_raw_columns_sql() -> &'static str { +fn migrate_legacy_raw_columns_sql() -> &'static str { return r#"DO $$ BEGIN IF to_regclass('kb_sol_raw_transactions') IS NOT NULL THEN @@ -274,7 +284,7 @@ $$"#; } /// SQL conditionally renaming historical canonical transaction constraints. -pub(in crate::postgres) fn migrate_legacy_raw_constraints_sql() -> &'static str { +fn migrate_legacy_raw_constraints_sql() -> &'static str { return r#"DO $$ BEGIN IF to_regclass('kb_sol_raw_transactions') IS NOT NULL THEN @@ -291,7 +301,7 @@ $$"#; } /// SQL conditionally renaming historical canonical transaction indexes. -pub(in crate::postgres) fn migrate_legacy_raw_indexes_sql() -> &'static str { +fn migrate_legacy_raw_indexes_sql() -> &'static str { return r#"DO $$ BEGIN IF to_regclass('ux_kb_sol_raw_rpc_transactions_signature') IS NOT NULL AND to_regclass('ux_kb_sol_raw_transactions_signature') IS NULL THEN ALTER INDEX ux_kb_sol_raw_rpc_transactions_signature RENAME TO ux_kb_sol_raw_transactions_signature; END IF; @@ -303,7 +313,7 @@ $$"#; } /// SQL conditionally renaming historical core-to-raw lineage. -pub(in crate::postgres) fn migrate_legacy_core_lineage_sql() -> &'static str { +fn migrate_legacy_core_lineage_sql() -> &'static str { return r#"DO $$ BEGIN IF to_regclass('kb_sol_core_transactions') IS NOT NULL THEN @@ -320,7 +330,7 @@ $$"#; } /// SQL creating `kb_sol_raw_transactions`. -pub(in crate::postgres) fn create_table_kb_sol_raw_transactions_sql() -> &'static str { +fn create_table_kb_sol_raw_transactions_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_raw_transactions ( id BIGSERIAL, signature TEXT NOT NULL, @@ -344,32 +354,32 @@ pub(in crate::postgres) fn create_table_kb_sol_raw_transactions_sql() -> &'stati } /// SQL creating the unique signature index for canonical raw transactions. -pub(in crate::postgres) fn create_ux_kb_sol_raw_transactions_signature_sql() -> &'static str { +fn create_ux_kb_sol_raw_transactions_signature_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_raw_transactions_signature ON kb_sol_raw_transactions (signature)"; } /// SQL creating the slot index for canonical raw transactions. -pub(in crate::postgres) fn create_ix_kb_sol_raw_transactions_slot_sql() -> &'static str { +fn create_ix_kb_sol_raw_transactions_slot_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_raw_transactions_slot ON kb_sol_raw_transactions (slot)"; } /// SQL creating the created timestamp index for canonical raw transactions. -pub(in crate::postgres) fn create_ix_kb_sol_raw_transactions_created_at_sql() -> &'static str { +fn create_ix_kb_sol_raw_transactions_created_at_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_raw_transactions_created_at ON kb_sol_raw_transactions (created_at)"; } /// SQL creating the processing state index for canonical raw transactions. -pub(in crate::postgres) fn create_ix_kb_sol_raw_transactions_processing_sql() -> &'static str { +fn create_ix_kb_sol_raw_transactions_processing_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_raw_transactions_processing ON kb_sol_raw_transactions (processing_state)"; } /// SQL creating the optional canonical document hash index. -pub(in crate::postgres) fn create_ix_kb_sol_raw_transactions_canonical_hash_sql() -> &'static str { +fn create_ix_kb_sol_raw_transactions_canonical_hash_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_raw_transactions_canonical_hash ON kb_sol_raw_transactions (canonical_json_hash) WHERE canonical_json_hash IS NOT NULL"; } /// SQL creating `kb_sol_obs_transaction_observations`. -pub(in crate::postgres) fn create_table_kb_sol_obs_transaction_observations_sql() -> &'static str { +fn create_table_kb_sol_obs_transaction_observations_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_obs_transaction_observations ( id BIGSERIAL, raw_transaction_id BIGINT, @@ -415,48 +425,42 @@ pub(in crate::postgres) fn create_table_kb_sol_obs_transaction_observations_sql( } /// SQL creating the unique observation key index. -pub(in crate::postgres) fn create_ux_kb_sol_obs_transaction_observations_key_sql() -> &'static str { +fn create_ux_kb_sol_obs_transaction_observations_key_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_obs_transaction_observations_key ON kb_sol_obs_transaction_observations (observation_key)"; } /// SQL creating the optional signature index for transaction observations. -pub(in crate::postgres) fn create_ix_kb_sol_obs_transaction_observations_signature_sql() --> &'static str { +fn create_ix_kb_sol_obs_transaction_observations_signature_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_obs_transaction_observations_signature ON kb_sol_obs_transaction_observations (signature) WHERE signature IS NOT NULL"; } /// SQL creating the optional slot index for transaction observations. -pub(in crate::postgres) fn create_ix_kb_sol_obs_transaction_observations_slot_sql() -> &'static str -{ +fn create_ix_kb_sol_obs_transaction_observations_slot_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_obs_transaction_observations_slot ON kb_sol_obs_transaction_observations (slot) WHERE slot IS NOT NULL"; } /// SQL creating the provider and acquisition method index for transaction observations. -pub(in crate::postgres) fn create_ix_kb_sol_obs_transaction_observations_provider_method_sql() --> &'static str { +fn create_ix_kb_sol_obs_transaction_observations_provider_method_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_obs_transaction_observations_provider_method ON kb_sol_obs_transaction_observations (provider, acquisition_method)"; } /// SQL creating the received timestamp index for transaction observations. -pub(in crate::postgres) fn create_ix_kb_sol_obs_transaction_observations_received_at_sql() --> &'static str { +fn create_ix_kb_sol_obs_transaction_observations_received_at_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_obs_transaction_observations_received_at ON kb_sol_obs_transaction_observations (received_at)"; } /// SQL creating the optional canonical transaction lineage index for observations. -pub(in crate::postgres) fn create_ix_kb_sol_obs_transaction_observations_raw_transaction_sql() --> &'static str { +fn create_ix_kb_sol_obs_transaction_observations_raw_transaction_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_obs_transaction_observations_raw_transaction ON kb_sol_obs_transaction_observations (raw_transaction_id) WHERE raw_transaction_id IS NOT NULL"; } /// SQL creating the status index for transaction observations. -pub(in crate::postgres) fn create_ix_kb_sol_obs_transaction_observations_status_sql() -> &'static str -{ +fn create_ix_kb_sol_obs_transaction_observations_status_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_obs_transaction_observations_status ON kb_sol_obs_transaction_observations (status)"; } /// SQL migrating lightweight metadata and dropping the historical WebSocket payload table. -pub(in crate::postgres) fn migrate_and_drop_legacy_ws_notifications_sql() -> &'static str { +fn migrate_and_drop_legacy_ws_notifications_sql() -> &'static str { return r#"DO $$ BEGIN IF to_regclass('kb_sol_raw_ws_notifications') IS NOT NULL THEN @@ -471,7 +475,7 @@ $$"#; } /// SQL creating `kb_sol_core_transactions`. -pub(in crate::postgres) fn create_table_kb_sol_core_transactions_sql() -> &'static str { +fn create_table_kb_sol_core_transactions_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_core_transactions ( id BIGSERIAL, raw_transaction_id BIGINT, @@ -489,22 +493,22 @@ pub(in crate::postgres) fn create_table_kb_sol_core_transactions_sql() -> &'stat } /// SQL creating the unique signature index for core transactions. -pub(in crate::postgres) fn create_ux_kb_sol_core_transactions_signature_sql() -> &'static str { +fn create_ux_kb_sol_core_transactions_signature_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_core_transactions_signature ON kb_sol_core_transactions (signature)"; } /// SQL creating the slot index for core transactions. -pub(in crate::postgres) fn create_ix_kb_sol_core_transactions_slot_sql() -> &'static str { +fn create_ix_kb_sol_core_transactions_slot_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_transactions_slot ON kb_sol_core_transactions (slot)"; } /// SQL creating the created_at index for core transactions. -pub(in crate::postgres) fn create_ix_kb_sol_core_transactions_created_at_sql() -> &'static str { +fn create_ix_kb_sol_core_transactions_created_at_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_transactions_created_at ON kb_sol_core_transactions (created_at)"; } /// SQL creating `kb_sol_core_account_keys`. -pub(in crate::postgres) fn create_table_kb_sol_core_account_keys_sql() -> &'static str { +fn create_table_kb_sol_core_account_keys_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_core_account_keys ( id BIGSERIAL, transaction_id BIGINT NOT NULL, @@ -528,17 +532,17 @@ pub(in crate::postgres) fn create_table_kb_sol_core_account_keys_sql() -> &'stat } /// SQL creating the unique signature/account index for core account keys. -pub(in crate::postgres) fn create_ux_kb_sol_core_account_keys_sig_index_sql() -> &'static str { +fn create_ux_kb_sol_core_account_keys_sig_index_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_core_account_keys_sig_index ON kb_sol_core_account_keys (signature, account_index)"; } /// SQL creating the account key index for core account keys. -pub(in crate::postgres) fn create_ix_kb_sol_core_account_keys_key_sql() -> &'static str { +fn create_ix_kb_sol_core_account_keys_key_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_account_keys_key ON kb_sol_core_account_keys (account_key)"; } /// SQL creating `kb_sol_core_instructions`. -pub(in crate::postgres) fn create_table_kb_sol_core_instructions_sql() -> &'static str { +fn create_table_kb_sol_core_instructions_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_core_instructions ( id BIGSERIAL, transaction_id BIGINT NOT NULL, @@ -566,27 +570,27 @@ pub(in crate::postgres) fn create_table_kb_sol_core_instructions_sql() -> &'stat } /// SQL creating the unique signature/path index for core instructions. -pub(in crate::postgres) fn create_ux_kb_sol_core_instructions_sig_path_sql() -> &'static str { +fn create_ux_kb_sol_core_instructions_sig_path_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_core_instructions_sig_path ON kb_sol_core_instructions (signature, instruction_path)"; } /// SQL creating the program index for core instructions. -pub(in crate::postgres) fn create_ix_kb_sol_core_instructions_program_sql() -> &'static str { +fn create_ix_kb_sol_core_instructions_program_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_instructions_program ON kb_sol_core_instructions (program_id)"; } /// SQL creating the slot index for core instructions. -pub(in crate::postgres) fn create_ix_kb_sol_core_instructions_slot_sql() -> &'static str { +fn create_ix_kb_sol_core_instructions_slot_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_instructions_slot ON kb_sol_core_instructions (slot)"; } /// SQL creating the processing state index for core instructions. -pub(in crate::postgres) fn create_ix_kb_sol_core_instructions_processing_sql() -> &'static str { +fn create_ix_kb_sol_core_instructions_processing_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_instructions_processing ON kb_sol_core_instructions (processing_state)"; } /// SQL creating `kb_sol_core_inner_instructions`. -pub(in crate::postgres) fn create_table_kb_sol_core_inner_instructions_sql() -> &'static str { +fn create_table_kb_sol_core_inner_instructions_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_core_inner_instructions ( id BIGSERIAL, transaction_id BIGINT NOT NULL, @@ -610,22 +614,22 @@ pub(in crate::postgres) fn create_table_kb_sol_core_inner_instructions_sql() -> } /// SQL creating the unique signature/path index for core inner instructions. -pub(in crate::postgres) fn create_ux_kb_sol_core_inner_instructions_sig_path_sql() -> &'static str { +fn create_ux_kb_sol_core_inner_instructions_sig_path_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_core_inner_instructions_sig_path ON kb_sol_core_inner_instructions (signature, instruction_path)"; } /// SQL creating the parent instruction index for core inner instructions. -pub(in crate::postgres) fn create_ix_kb_sol_core_inner_instructions_parent_sql() -> &'static str { +fn create_ix_kb_sol_core_inner_instructions_parent_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_inner_instructions_parent ON kb_sol_core_inner_instructions (signature, parent_instruction_path)"; } /// SQL creating the program index for core inner instructions. -pub(in crate::postgres) fn create_ix_kb_sol_core_inner_instructions_program_sql() -> &'static str { +fn create_ix_kb_sol_core_inner_instructions_program_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_inner_instructions_program ON kb_sol_core_inner_instructions (program_id)"; } /// SQL creating `kb_sol_core_logs`. -pub(in crate::postgres) fn create_table_kb_sol_core_logs_sql() -> &'static str { +fn create_table_kb_sol_core_logs_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_core_logs ( id BIGSERIAL, transaction_id BIGINT NOT NULL, @@ -649,22 +653,22 @@ pub(in crate::postgres) fn create_table_kb_sol_core_logs_sql() -> &'static str { } /// SQL creating the unique signature/log index for core logs. -pub(in crate::postgres) fn create_ux_kb_sol_core_logs_sig_index_sql() -> &'static str { +fn create_ux_kb_sol_core_logs_sig_index_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_core_logs_sig_index ON kb_sol_core_logs (signature, log_index)"; } /// SQL creating the program index for core logs. -pub(in crate::postgres) fn create_ix_kb_sol_core_logs_program_sql() -> &'static str { +fn create_ix_kb_sol_core_logs_program_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_logs_program ON kb_sol_core_logs (program_id) WHERE program_id IS NOT NULL"; } /// SQL creating the instruction path index for core logs. -pub(in crate::postgres) fn create_ix_kb_sol_core_logs_path_sql() -> &'static str { +fn create_ix_kb_sol_core_logs_path_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_logs_path ON kb_sol_core_logs (signature, instruction_path) WHERE instruction_path IS NOT NULL"; } /// SQL creating `kb_sol_core_balance_changes`. -pub(in crate::postgres) fn create_table_kb_sol_core_balance_changes_sql() -> &'static str { +fn create_table_kb_sol_core_balance_changes_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_core_balance_changes ( id BIGSERIAL, transaction_id BIGINT NOT NULL, @@ -694,27 +698,28 @@ pub(in crate::postgres) fn create_table_kb_sol_core_balance_changes_sql() -> &'s } /// SQL creating the unique signature/balance index for core balance changes. -pub(in crate::postgres) fn create_ux_kb_sol_core_balance_changes_sig_index_sql() -> &'static str { +fn create_ux_kb_sol_core_balance_changes_sig_index_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_core_balance_changes_sig_index ON kb_sol_core_balance_changes (signature, balance_change_index)"; } /// SQL creating the account key index for core balance changes. -pub(in crate::postgres) fn create_ix_kb_sol_core_balance_changes_account_sql() -> &'static str { +fn create_ix_kb_sol_core_balance_changes_account_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_balance_changes_account ON kb_sol_core_balance_changes (account_key) WHERE account_key IS NOT NULL"; } /// SQL creating the mint index for core balance changes. -pub(in crate::postgres) fn create_ix_kb_sol_core_balance_changes_mint_sql() -> &'static str { +fn create_ix_kb_sol_core_balance_changes_mint_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_core_balance_changes_mint ON kb_sol_core_balance_changes (mint) WHERE mint IS NOT NULL"; } /// Returns true when a Solana table name follows the canonical prefix and domain rules. -pub fn is_valid_solana_table_name(table_name: &str) -> bool { - return crate::validate_solana_table_name(table_name).is_ok(); +#[cfg(test)] +fn is_valid_solana_table_name(table_name: &str) -> bool { + return validate_solana_table_name(table_name).is_ok(); } /// Validates a canonical Solana table name. -pub fn validate_solana_table_name(table_name: &str) -> ks_core::Result<()> { +fn validate_solana_table_name(table_name: &str) -> ks_core::Result<()> { let trimmed_table_name = table_name.trim(); if trimmed_table_name.is_empty() { return std::result::Result::Err(ks_core::Error::db("table name must not be empty")); @@ -724,12 +729,12 @@ pub fn validate_solana_table_name(table_name: &str) -> ks_core::Result<()> { "solana table names must not contain an explicit PostgreSQL schema", )); } - if !trimmed_table_name.starts_with(crate::SOLANA_TABLE_PREFIX) { + if !trimmed_table_name.starts_with(SOLANA_TABLE_PREFIX) { return std::result::Result::Err(ks_core::Error::db( "solana table name must start with kb_sol_", )); } - let suffix = &trimmed_table_name[crate::SOLANA_TABLE_PREFIX.len()..]; + let suffix = &trimmed_table_name[SOLANA_TABLE_PREFIX.len()..]; let domain = crate::postgres::migrations::first_segment(suffix); if domain.is_empty() { return std::result::Result::Err(ks_core::Error::db( @@ -755,23 +760,23 @@ pub fn validate_solana_table_name(table_name: &str) -> ks_core::Result<()> { } /// Validates the raw store table names introduced by `0.2.3`. -pub fn validate_raw_store_table_names() -> ks_core::Result<()> { - return crate::postgres::migrations::validate_table_names(crate::RAW_STORE_TABLE_NAMES); +pub(crate) fn validate_raw_store_table_names() -> ks_core::Result<()> { + return crate::postgres::migrations::validate_table_names(RAW_STORE_TABLE_NAMES); } /// Validates the core store table names introduced by `0.2.4`. -pub fn validate_core_store_table_names() -> ks_core::Result<()> { - return crate::postgres::migrations::validate_table_names(crate::CORE_STORE_TABLE_NAMES); +pub(crate) fn validate_core_store_table_names() -> ks_core::Result<()> { + return crate::postgres::migrations::validate_table_names(CORE_STORE_TABLE_NAMES); } /// Validates the decode and materialization table names introduced by `0.4.0`. -pub fn validate_decode_store_table_names() -> ks_core::Result<()> { - return crate::postgres::migrations::validate_table_names(crate::DECODE_STORE_TABLE_NAMES); +pub(crate) fn validate_decode_store_table_names() -> ks_core::Result<()> { + return crate::postgres::migrations::validate_table_names(DECODE_STORE_TABLE_NAMES); } fn validate_table_names(table_names: &[&str]) -> ks_core::Result<()> { for table_name in table_names { - let validation_result = crate::validate_solana_table_name(table_name); + let validation_result = validate_solana_table_name(table_name); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); } @@ -787,7 +792,7 @@ fn first_segment(value: &str) -> &str { } fn is_allowed_domain(domain: &str) -> bool { - for allowed_domain in crate::ALLOWED_SOLANA_TABLE_DOMAINS { + for allowed_domain in ALLOWED_SOLANA_TABLE_DOMAINS { if domain == *allowed_domain { return true; } @@ -812,7 +817,7 @@ fn contains_only_table_name_chars(table_name: &str) -> bool { } /// SQL creating `kb_sol_ops_processing_ledger`. -pub(in crate::postgres) fn create_table_kb_sol_ops_processing_ledger_sql() -> &'static str { +fn create_table_kb_sol_ops_processing_ledger_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_ops_processing_ledger ( id BIGSERIAL, stage TEXT NOT NULL, @@ -842,87 +847,87 @@ pub(in crate::postgres) fn create_table_kb_sol_ops_processing_ledger_sql() -> &' } /// SQL creating the unique processing ledger identity index. -pub(in crate::postgres) fn create_ux_kb_sol_ops_processing_ledger_identity_sql() -> &'static str { +fn create_ux_kb_sol_ops_processing_ledger_identity_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_ops_processing_ledger_identity ON kb_sol_ops_processing_ledger (stage, processor_name, processor_version, input_key)"; } /// SQL creating the processing ledger status index. -pub(in crate::postgres) fn create_ix_kb_sol_ops_processing_ledger_status_sql() -> &'static str { +fn create_ix_kb_sol_ops_processing_ledger_status_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_ops_processing_ledger_status ON kb_sol_ops_processing_ledger (stage, processor_name, status, updated_at)"; } /// SQL creating the processing ledger input hash index. -pub(in crate::postgres) fn create_ix_kb_sol_ops_processing_ledger_input_hash_sql() -> &'static str { +fn create_ix_kb_sol_ops_processing_ledger_input_hash_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_ops_processing_ledger_input_hash ON kb_sol_ops_processing_ledger (input_hash)"; } /// SQL statistics query for `kb_sol_raw_transactions`. -pub(in crate::postgres) fn table_stats_kb_sol_raw_transactions_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_raw_transactions_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_raw_transactions"; } /// SQL statistics query for `kb_sol_obs_transaction_observations`. -pub(in crate::postgres) fn table_stats_kb_sol_obs_transaction_observations_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_obs_transaction_observations_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(persisted_at)::text AS latest_created_at FROM kb_sol_obs_transaction_observations"; } /// SQL statistics query for `kb_sol_core_transactions`. -pub(in crate::postgres) fn table_stats_kb_sol_core_transactions_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_core_transactions_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_core_transactions"; } /// SQL statistics query for `kb_sol_core_account_keys`. -pub(in crate::postgres) fn table_stats_kb_sol_core_account_keys_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_core_account_keys_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_core_account_keys"; } /// SQL statistics query for `kb_sol_core_instructions`. -pub(in crate::postgres) fn table_stats_kb_sol_core_instructions_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_core_instructions_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_core_instructions"; } /// SQL statistics query for `kb_sol_core_inner_instructions`. -pub(in crate::postgres) fn table_stats_kb_sol_core_inner_instructions_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_core_inner_instructions_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_core_inner_instructions"; } /// SQL statistics query for `kb_sol_core_logs`. -pub(in crate::postgres) fn table_stats_kb_sol_core_logs_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_core_logs_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_core_logs"; } /// SQL statistics query for `kb_sol_core_balance_changes`. -pub(in crate::postgres) fn table_stats_kb_sol_core_balance_changes_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_core_balance_changes_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_core_balance_changes"; } /// SQL statistics query for `kb_sol_ops_processing_ledger`. -pub(in crate::postgres) fn table_stats_kb_sol_ops_processing_ledger_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_ops_processing_ledger_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, NULL::BIGINT AS min_slot, NULL::BIGINT AS max_slot, MAX(updated_at)::text AS latest_created_at FROM kb_sol_ops_processing_ledger"; } /// SQL statistics query for `kb_sol_decode_events`. -pub(in crate::postgres) fn table_stats_kb_sol_decode_events_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_decode_events_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_decode_events"; } /// SQL statistics query for `kb_sol_decode_coverage_declarations`. -pub(in crate::postgres) fn table_stats_kb_sol_decode_coverage_declarations_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_decode_coverage_declarations_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, NULL::BIGINT AS min_slot, NULL::BIGINT AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_decode_coverage_declarations"; } /// SQL statistics query for `kb_sol_decode_coverage_observations`. -pub(in crate::postgres) fn table_stats_kb_sol_decode_coverage_observations_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_decode_coverage_observations_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_decode_coverage_observations"; } /// SQL statistics query for `kb_sol_mat_events`. -pub(in crate::postgres) fn table_stats_kb_sol_mat_events_sql() -> &'static str { +pub(crate) fn table_stats_kb_sol_mat_events_sql() -> &'static str { return "SELECT COUNT(*)::BIGINT AS row_count, MIN(slot) AS min_slot, MAX(slot) AS max_slot, MAX(created_at)::text AS latest_created_at FROM kb_sol_mat_events"; } /// SQL creating `kb_sol_decode_events`. -pub(in crate::postgres) fn create_table_kb_sol_decode_events_sql() -> &'static str { +fn create_table_kb_sol_decode_events_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_decode_events ( id BIGSERIAL, processor_name TEXT NOT NULL, processor_version TEXT NOT NULL, input_key TEXT NOT NULL, input_hash TEXT NOT NULL, event_key TEXT NOT NULL, @@ -940,27 +945,27 @@ pub(in crate::postgres) fn create_table_kb_sol_decode_events_sql() -> &'static s } /// SQL creating the decoded observation stable identity index. -pub(in crate::postgres) fn create_ux_kb_sol_decode_events_identity_sql() -> &'static str { +fn create_ux_kb_sol_decode_events_identity_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_decode_events_processor_input_event ON kb_sol_decode_events (processor_name, processor_version, input_key, event_key)"; } /// SQL creating the decoded observation signature and path index. -pub(in crate::postgres) fn create_ix_kb_sol_decode_events_signature_path_sql() -> &'static str { +fn create_ix_kb_sol_decode_events_signature_path_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_decode_events_signature_path ON kb_sol_decode_events (signature, instruction_path)"; } /// SQL creating the decoded observation program and surface index. -pub(in crate::postgres) fn create_ix_kb_sol_decode_events_program_surface_sql() -> &'static str { +fn create_ix_kb_sol_decode_events_program_surface_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_decode_events_program_surface ON kb_sol_decode_events (program_id, surface_code, event_code)"; } /// SQL creating the decoded observation family and commit index. -pub(in crate::postgres) fn create_ix_kb_sol_decode_events_family_commit_sql() -> &'static str { +fn create_ix_kb_sol_decode_events_family_commit_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_decode_events_family_commit ON kb_sol_decode_events (event_family, transaction_failed, observation_committed)"; } /// SQL creating `kb_sol_decode_coverage_declarations`. -pub(in crate::postgres) fn create_table_kb_sol_decode_coverage_declarations_sql() -> &'static str { +fn create_table_kb_sol_decode_coverage_declarations_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_decode_coverage_declarations ( id BIGSERIAL, processor_name TEXT NOT NULL, processor_version TEXT NOT NULL, program_id TEXT NOT NULL, surface_code TEXT, entry_kind TEXT NOT NULL, @@ -971,19 +976,17 @@ pub(in crate::postgres) fn create_table_kb_sol_decode_coverage_declarations_sql( } /// SQL creating the declared coverage stable identity index. -pub(in crate::postgres) fn create_ux_kb_sol_decode_coverage_declarations_identity_sql() --> &'static str { +fn create_ux_kb_sol_decode_coverage_declarations_identity_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_decode_coverage_declarations_identity ON kb_sol_decode_coverage_declarations (processor_name, processor_version, program_id, COALESCE(surface_code, ''), entry_kind, entry_code, COALESCE(discriminator_hex, ''))"; } /// SQL creating the declared coverage program index. -pub(in crate::postgres) fn create_ix_kb_sol_decode_coverage_declarations_program_sql() --> &'static str { +fn create_ix_kb_sol_decode_coverage_declarations_program_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_decode_coverage_declarations_program ON kb_sol_decode_coverage_declarations (program_id, processor_name, processor_version)"; } /// SQL creating `kb_sol_decode_coverage_observations`. -pub(in crate::postgres) fn create_table_kb_sol_decode_coverage_observations_sql() -> &'static str { +fn create_table_kb_sol_decode_coverage_observations_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_decode_coverage_observations ( id BIGSERIAL, processor_name TEXT NOT NULL, processor_version TEXT NOT NULL, input_key TEXT NOT NULL, input_hash TEXT NOT NULL, signature TEXT NOT NULL, @@ -1000,25 +1003,22 @@ pub(in crate::postgres) fn create_table_kb_sol_decode_coverage_observations_sql( } /// SQL creating the observed coverage stable identity index. -pub(in crate::postgres) fn create_ux_kb_sol_decode_coverage_observations_identity_sql() --> &'static str { +fn create_ux_kb_sol_decode_coverage_observations_identity_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_decode_coverage_observations_identity ON kb_sol_decode_coverage_observations (processor_name, processor_version, input_key)"; } /// SQL creating the observed coverage program and status index. -pub(in crate::postgres) fn create_ix_kb_sol_decode_coverage_observations_program_status_sql() --> &'static str { +fn create_ix_kb_sol_decode_coverage_observations_program_status_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_decode_coverage_observations_program_status ON kb_sol_decode_coverage_observations (program_id, status, transaction_failed)"; } /// SQL creating the observed coverage entry index. -pub(in crate::postgres) fn create_ix_kb_sol_decode_coverage_observations_entry_sql() -> &'static str -{ +fn create_ix_kb_sol_decode_coverage_observations_entry_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_decode_coverage_observations_entry ON kb_sol_decode_coverage_observations (processor_name, processor_version, surface_code, entry_code)"; } /// SQL creating `kb_sol_mat_events`. -pub(in crate::postgres) fn create_table_kb_sol_mat_events_sql() -> &'static str { +fn create_table_kb_sol_mat_events_sql() -> &'static str { return r#"CREATE TABLE IF NOT EXISTS kb_sol_mat_events ( id BIGSERIAL, processor_name TEXT NOT NULL, processor_version TEXT NOT NULL, input_key TEXT NOT NULL, input_hash TEXT NOT NULL, output_key TEXT NOT NULL, @@ -1033,12 +1033,12 @@ pub(in crate::postgres) fn create_table_kb_sol_mat_events_sql() -> &'static str } /// SQL creating the materialized output stable identity index. -pub(in crate::postgres) fn create_ux_kb_sol_mat_events_identity_sql() -> &'static str { +fn create_ux_kb_sol_mat_events_identity_sql() -> &'static str { return "CREATE UNIQUE INDEX IF NOT EXISTS ux_kb_sol_mat_events_processor_input_output ON kb_sol_mat_events (processor_name, processor_version, input_key, output_key)"; } /// SQL creating materialized output signature and family indexes. -pub(in crate::postgres) fn create_ix_kb_sol_mat_events_signature_family_sql() -> &'static str { +fn create_ix_kb_sol_mat_events_signature_family_sql() -> &'static str { return "CREATE INDEX IF NOT EXISTS ix_kb_sol_mat_events_signature_family ON kb_sol_mat_events (source_decoder_name, source_decoder_version, source_decode_input_key, signature, materialized_family)"; } @@ -1046,28 +1046,28 @@ pub(in crate::postgres) fn create_ix_kb_sol_mat_events_signature_family_sql() -> mod tests { #[test] fn valid_table_name_accepts_domain_prefix() { - assert!(crate::is_valid_solana_table_name("kb_sol_raw_transactions")); + assert!(super::is_valid_solana_table_name("kb_sol_raw_transactions")); } #[test] fn table_name_rejects_explicit_schema() { let invalid_name = std::string::String::from("raw") + "." + "kb_sol_rpc_transactions"; - assert!(!crate::is_valid_solana_table_name(invalid_name.as_str())); + assert!(!super::is_valid_solana_table_name(invalid_name.as_str())); } #[test] fn table_name_rejects_unknown_domain() { - assert!(!crate::is_valid_solana_table_name("kb_sol_unknown_rows")); + assert!(!super::is_valid_solana_table_name("kb_sol_unknown_rows")); } #[test] fn table_name_rejects_missing_table_suffix() { - assert!(!crate::is_valid_solana_table_name("kb_sol_raw")); + assert!(!super::is_valid_solana_table_name("kb_sol_raw")); } #[test] fn table_name_rejects_uppercase() { - assert!(!crate::is_valid_solana_table_name("kb_sol_raw_RPC_transactions")); + assert!(!super::is_valid_solana_table_name("kb_sol_raw_RPC_transactions")); } #[test] diff --git a/ks-store/src/postgres/query.rs b/ks-store/src/postgres/query.rs index 45b506c..3c1e37f 100644 --- a/ks-store/src/postgres/query.rs +++ b/ks-store/src/postgres/query.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/query.rs -// version: 3 +// version: 4 //! PostgreSQL query modules. @@ -11,42 +11,43 @@ mod raw_queries; mod replay_candidate_queries; mod table_diagnostics_queries; -pub(in crate::postgres) use self::core_extraction_queries::is_core_extraction_current; -pub(in crate::postgres) use self::core_extraction_queries::list_raw_transactions_for_core_extraction; -pub(in crate::postgres) use self::core_extraction_queries::mark_core_extraction_failed; -pub(in crate::postgres) use self::core_extraction_queries::persist_core_extraction; -pub(in crate::postgres) use self::core_queries::apply_core_store_schema; -pub(in crate::postgres) use self::core_queries::insert_core_account_keys; -pub(in crate::postgres) use self::core_queries::insert_core_balance_changes; -pub(in crate::postgres) use self::core_queries::insert_core_inner_instructions; -pub(in crate::postgres) use self::core_queries::insert_core_instructions; -pub(in crate::postgres) use self::core_queries::insert_core_logs; -pub(in crate::postgres) use self::core_queries::insert_core_transaction; -pub(in crate::postgres) use self::core_queries::list_core_instruction_replay_inputs; -pub(in crate::postgres) use self::core_queries::list_core_instructions_for_replay; -pub(in crate::postgres) use self::core_queries::update_core_instruction_lifecycle; -pub(in crate::postgres) use self::decode_pipeline_queries::apply_decode_store_schema; -pub(in crate::postgres) use self::decode_pipeline_queries::is_decode_current; -pub(in crate::postgres) use self::decode_pipeline_queries::list_decode_coverage_summary; -pub(in crate::postgres) use self::decode_pipeline_queries::list_decode_inputs; -pub(in crate::postgres) use self::decode_pipeline_queries::list_materialized_events; -pub(in crate::postgres) use self::decode_pipeline_queries::mark_decode_failed; -pub(in crate::postgres) use self::decode_pipeline_queries::persist_decode_coverage_declarations; -pub(in crate::postgres) use self::decode_pipeline_queries::persist_decode_result; -pub(in crate::postgres) use self::decode_pipeline_queries::persist_materialization_result; -pub(in crate::postgres) use self::health_queries::load_current_schema; -pub(in crate::postgres) use self::health_queries::load_latest_migration_version; -pub(in crate::postgres) use self::health_queries::load_migration_table_name; -pub(in crate::postgres) use self::health_queries::load_server_version; -pub(in crate::postgres) use self::health_queries::run_health_check; -pub(in crate::postgres) use self::raw_queries::apply_raw_store_schema; -pub(in crate::postgres) use self::raw_queries::has_raw_transaction_signature; -pub(in crate::postgres) use self::raw_queries::has_transaction_observation_key; -pub(in crate::postgres) use self::raw_queries::insert_raw_transaction; -pub(in crate::postgres) use self::raw_queries::insert_transaction_observation; -pub(in crate::postgres) use self::raw_queries::update_raw_payload_lifecycle; -pub(in crate::postgres) use self::replay_candidate_queries::list_replay_entity_summaries; -pub(in crate::postgres) use self::replay_candidate_queries::list_replay_program_summaries; -pub(in crate::postgres) use self::replay_candidate_queries::list_replay_transaction_candidates; -pub(in crate::postgres) use self::table_diagnostics_queries::load_table_statistics; -pub(in crate::postgres) use self::table_diagnostics_queries::table_exists; +pub(crate) use self::core_extraction_queries::is_core_extraction_current; +pub(crate) use self::core_extraction_queries::list_raw_transactions_for_core_extraction; +pub(crate) use self::core_extraction_queries::mark_core_extraction_failed; +pub(crate) use self::core_extraction_queries::persist_core_extraction; +pub(crate) use self::core_queries::apply_core_store_schema; +pub(crate) use self::core_queries::insert_core_account_keys; +pub(crate) use self::core_queries::insert_core_balance_changes; +pub(crate) use self::core_queries::insert_core_inner_instructions; +pub(crate) use self::core_queries::insert_core_instructions; +pub(crate) use self::core_queries::insert_core_logs; +pub(crate) use self::core_queries::insert_core_transaction; +pub(crate) use self::core_queries::list_core_instruction_replay_inputs; +pub(crate) use self::core_queries::list_core_instructions_for_replay; +pub(crate) use self::core_queries::list_decode_replay_inputs; +pub(crate) use self::core_queries::update_core_instruction_lifecycle; +pub(crate) use self::decode_pipeline_queries::apply_decode_store_schema; +pub(crate) use self::decode_pipeline_queries::is_decode_current; +pub(crate) use self::decode_pipeline_queries::list_decode_coverage_summary; +pub(crate) use self::decode_pipeline_queries::list_decode_inputs; +pub(crate) use self::decode_pipeline_queries::list_materialized_events; +pub(crate) use self::decode_pipeline_queries::mark_decode_failed; +pub(crate) use self::decode_pipeline_queries::persist_decode_coverage_declarations; +pub(crate) use self::decode_pipeline_queries::persist_decode_result; +pub(crate) use self::decode_pipeline_queries::persist_materialization_result; +pub(crate) use self::health_queries::load_current_schema; +pub(crate) use self::health_queries::load_latest_migration_version; +pub(crate) use self::health_queries::load_migration_table_name; +pub(crate) use self::health_queries::load_server_version; +pub(crate) use self::health_queries::run_health_check; +pub(crate) use self::raw_queries::apply_raw_store_schema; +pub(crate) use self::raw_queries::has_raw_transaction_signature; +pub(crate) use self::raw_queries::has_transaction_observation_key; +pub(crate) use self::raw_queries::insert_raw_transaction; +pub(crate) use self::raw_queries::insert_transaction_observation; +pub(crate) use self::raw_queries::update_raw_payload_lifecycle; +pub(crate) use self::replay_candidate_queries::list_replay_entity_summaries; +pub(crate) use self::replay_candidate_queries::list_replay_program_summaries; +pub(crate) use self::replay_candidate_queries::list_replay_transaction_candidates; +pub(crate) use self::table_diagnostics_queries::load_table_statistics; +pub(crate) use self::table_diagnostics_queries::table_exists; diff --git a/ks-store/src/postgres/query/core_extraction_queries.rs b/ks-store/src/postgres/query/core_extraction_queries.rs index 9450c4f..634f8e9 100644 --- a/ks-store/src/postgres/query/core_extraction_queries.rs +++ b/ks-store/src/postgres/query/core_extraction_queries.rs @@ -1,11 +1,11 @@ // file: ks-store/src/postgres/query/core_extraction_queries.rs -// version: 4 +// version: 6 //! PostgreSQL queries for atomic canonical transaction to core extraction. use sqlx::Row; // rust-rules: trait-import -pub(in crate::postgres) async fn list_raw_transactions_for_core_extraction( +pub(crate) async fn list_raw_transactions_for_core_extraction( pool: &sqlx::PgPool, filter: &crate::CoreExtractionSelectionFilter, ) -> ks_core::Result> { @@ -59,7 +59,7 @@ pub(in crate::postgres) async fn list_raw_transactions_for_core_extraction( return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn is_core_extraction_current( +pub(crate) async fn is_core_extraction_current( pool: &sqlx::PgPool, identity: &crate::ProcessingLedgerIdentity, ) -> ks_core::Result { @@ -81,7 +81,7 @@ pub(in crate::postgres) async fn is_core_extraction_current( }; } -pub(in crate::postgres) async fn persist_core_extraction( +pub(crate) async fn persist_core_extraction( pool: &sqlx::PgPool, bundle: &crate::CoreExtractionBundle, _force_replay: bool, @@ -126,11 +126,7 @@ pub(in crate::postgres) async fn persist_core_extraction( } } let transaction_id_result = - crate::postgres::query::core_extraction_queries::insert_core_transaction_in_transaction( - &mut transaction, - &bundle.transaction, - ) - .await; + insert_core_transaction_in_transaction(&mut transaction, &bundle.transaction).await; let transaction_id = match transaction_id_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -226,7 +222,7 @@ pub(in crate::postgres) async fn persist_core_extraction( )); } -pub(in crate::postgres) async fn mark_core_extraction_failed( +pub(crate) async fn mark_core_extraction_failed( pool: &sqlx::PgPool, failure: &crate::CoreExtractionFailure, ) -> ks_core::Result { @@ -783,7 +779,7 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => return, }; - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false); let options = match options_result { std::result::Result::Ok(value) => value, diff --git a/ks-store/src/postgres/query/core_queries.rs b/ks-store/src/postgres/query/core_queries.rs index ea80605..99a80f0 100644 --- a/ks-store/src/postgres/query/core_queries.rs +++ b/ks-store/src/postgres/query/core_queries.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/query/core_queries.rs -// version: 4 +// version: 6 //! PostgreSQL queries for normalized Solana core storage. @@ -7,9 +7,7 @@ use sqlx::Row; // rust-rules: trait-import const OUTER_INSTRUCTIONS_CONTEXT_SQL: &str = "SELECT COALESCE(jsonb_agg(jsonb_build_object('instructionIndex', instruction_path::BIGINT, 'instructionPath', instruction_path, 'programId', program_id, 'payloadJson', payload_json, 'payloadHash', payload_json_hash) ORDER BY instruction_path::BIGINT, instruction_path ASC), '[]'::jsonb) FROM kb_sol_core_instructions WHERE signature = $1 AND instruction_path ~ '^[0-9]+$'"; -pub(in crate::postgres) async fn apply_core_store_schema( - pool: &sqlx::PgPool, -) -> ks_core::Result<()> { +pub(crate) async fn apply_core_store_schema(pool: &sqlx::PgPool) -> ks_core::Result<()> { let validation_result = crate::validate_core_store_table_names(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); @@ -49,7 +47,7 @@ pub(in crate::postgres) async fn apply_core_store_schema( return std::result::Result::Ok(()); } -pub(in crate::postgres) async fn insert_core_transaction( +pub(crate) async fn insert_core_transaction( pool: &sqlx::PgPool, input: &crate::CoreTransactionInsert, ) -> ks_core::Result { @@ -74,7 +72,7 @@ pub(in crate::postgres) async fn insert_core_transaction( ); } -pub(in crate::postgres) async fn insert_core_account_keys( +pub(crate) async fn insert_core_account_keys( pool: &sqlx::PgPool, inputs: &[crate::CoreAccountKeyInsert], ) -> ks_core::Result { @@ -122,7 +120,7 @@ pub(in crate::postgres) async fn insert_core_account_keys( return std::result::Result::Ok(crate::InsertOutcome::new(inserted_count, 0, skipped_count)); } -pub(in crate::postgres) async fn insert_core_instructions( +pub(crate) async fn insert_core_instructions( pool: &sqlx::PgPool, inputs: &[crate::CoreInstructionInsert], ) -> ks_core::Result { @@ -165,7 +163,7 @@ pub(in crate::postgres) async fn insert_core_instructions( return std::result::Result::Ok(crate::InsertOutcome::new(inserted_count, 0, skipped_count)); } -pub(in crate::postgres) async fn insert_core_inner_instructions( +pub(crate) async fn insert_core_inner_instructions( pool: &sqlx::PgPool, inputs: &[crate::CoreInnerInstructionInsert], ) -> ks_core::Result { @@ -204,7 +202,7 @@ pub(in crate::postgres) async fn insert_core_inner_instructions( return std::result::Result::Ok(crate::InsertOutcome::new(inserted_count, 0, skipped_count)); } -pub(in crate::postgres) async fn insert_core_logs( +pub(crate) async fn insert_core_logs( pool: &sqlx::PgPool, inputs: &[crate::CoreLogInsert], ) -> ks_core::Result { @@ -250,7 +248,7 @@ pub(in crate::postgres) async fn insert_core_logs( return std::result::Result::Ok(crate::InsertOutcome::new(inserted_count, 0, skipped_count)); } -pub(in crate::postgres) async fn insert_core_balance_changes( +pub(crate) async fn insert_core_balance_changes( pool: &sqlx::PgPool, inputs: &[crate::CoreBalanceChangeInsert], ) -> ks_core::Result { @@ -310,7 +308,7 @@ pub(in crate::postgres) async fn insert_core_balance_changes( return std::result::Result::Ok(crate::InsertOutcome::new(inserted_count, 0, skipped_count)); } -pub(in crate::postgres) async fn list_core_instructions_for_replay( +pub(crate) async fn list_core_instructions_for_replay( pool: &sqlx::PgPool, filter: &crate::CoreInstructionReplayFilter, page_request: &crate::PageRequest, @@ -368,18 +366,13 @@ pub(in crate::postgres) async fn list_core_instructions_for_replay( return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn list_core_instruction_replay_inputs( +pub(crate) async fn list_core_instruction_replay_inputs( pool: &sqlx::PgPool, filter: &crate::CoreInstructionReplayFilter, page_request: &crate::PageRequest, ) -> ks_core::Result> { let instructions_result = - crate::postgres::query::core_queries::list_core_instructions_for_replay( - pool, - filter, - page_request, - ) - .await; + crate::list_core_instructions_for_replay(pool, filter, page_request).await; let instructions = match instructions_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -399,7 +392,7 @@ pub(in crate::postgres) async fn list_core_instruction_replay_inputs( return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn list_decode_replay_inputs( +pub(crate) async fn list_decode_replay_inputs( pool: &sqlx::PgPool, filter: &crate::DecodeSelectionFilter, ) -> ks_core::Result> { @@ -482,7 +475,7 @@ pub(in crate::postgres) async fn list_decode_replay_inputs( return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn update_core_instruction_lifecycle( +pub(crate) async fn update_core_instruction_lifecycle( pool: &sqlx::PgPool, mark: &crate::CoreInstructionLifecycleMark, ) -> ks_core::Result { @@ -507,7 +500,7 @@ pub(in crate::postgres) async fn update_core_instruction_lifecycle( ); } -pub(in crate::postgres) fn instruction_processing_state_to_sql( +fn instruction_processing_state_to_sql( state: crate::CoreInstructionProcessingState, ) -> &'static str { return match state { @@ -755,7 +748,7 @@ where }; } -pub(in crate::postgres) async fn load_replay_input_for_instruction( +async fn load_replay_input_for_instruction( pool: &sqlx::PgPool, instruction: &crate::CoreInstructionRow, ) -> ks_core::Result { @@ -990,7 +983,7 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => return, }; - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false); let options = match options_result { std::result::Result::Ok(value) => value, diff --git a/ks-store/src/postgres/query/decode_pipeline_queries.rs b/ks-store/src/postgres/query/decode_pipeline_queries.rs index 8fbff09..e8db070 100644 --- a/ks-store/src/postgres/query/decode_pipeline_queries.rs +++ b/ks-store/src/postgres/query/decode_pipeline_queries.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/query/decode_pipeline_queries.rs -// version: 7 +// version: 10 //! PostgreSQL queries for contextual decode, coverage and materialization persistence. @@ -22,10 +22,8 @@ struct MaterializedEventDatabaseRow { updated_at: std::string::String, } -pub(in crate::postgres) async fn apply_decode_store_schema( - pool: &sqlx::PgPool, -) -> ks_core::Result<()> { - tracing::debug!(target: crate::TRACING_TARGET, action = "apply_decode_store_schema", statement_count = crate::decode_store_schema_statements().len(), "apply PostgreSQL decode store schema"); +pub(crate) async fn apply_decode_store_schema(pool: &sqlx::PgPool) -> ks_core::Result<()> { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "apply_decode_store_schema", statement_count = crate::decode_store_schema_statements().len(), "apply PostgreSQL decode store schema"); let validation_result = crate::validate_decode_store_table_names(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); @@ -62,17 +60,16 @@ pub(in crate::postgres) async fn apply_decode_store_schema( "postgres decode store schema commit failed: {error}" ))); } - tracing::debug!(target: crate::TRACING_TARGET, action = "apply_decode_store_schema", committed = true, "PostgreSQL decode store schema applied"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "apply_decode_store_schema", committed = true, "PostgreSQL decode store schema applied"); return std::result::Result::Ok(()); } -pub(in crate::postgres) async fn list_decode_inputs( +pub(crate) async fn list_decode_inputs( pool: &sqlx::PgPool, filter: &crate::DecodeSelectionFilter, ) -> ks_core::Result> { - tracing::debug!(target: crate::TRACING_TARGET, action = "list_decode_inputs", signature_count = filter.signatures.len(), signature_sample = ?filter.signatures.iter().take(5).map(std::string::String::as_str).collect::>(), processing_states = ?filter.processing_states, min_slot = ?filter.min_slot, max_slot = ?filter.max_slot, program_ids = ?filter.program_ids, instruction_paths = ?filter.instruction_paths, limit = filter.limit, "query PostgreSQL contextual decode inputs"); - let result = - crate::postgres::query::core_queries::list_decode_replay_inputs(pool, filter).await; + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_decode_inputs", signature_count = filter.signatures.len(), signature_sample = ?filter.signatures.iter().take(5).map(std::string::String::as_str).collect::>(), processing_states = ?filter.processing_states, min_slot = ?filter.min_slot, max_slot = ?filter.max_slot, program_ids = ?filter.program_ids, instruction_paths = ?filter.instruction_paths, limit = filter.limit, "query PostgreSQL contextual decode inputs"); + let result = crate::list_decode_replay_inputs(pool, filter).await; return match result { std::result::Result::Ok(inputs) => { let selected_input_keys = inputs @@ -80,21 +77,21 @@ pub(in crate::postgres) async fn list_decode_inputs( .take(10) .map(|input| return input.replay_input_key.as_str()) .collect::>(); - tracing::debug!(target: crate::TRACING_TARGET, action = "list_decode_inputs", selected_count = inputs.len(), input_key_sample = ?selected_input_keys, "PostgreSQL contextual decode inputs selected"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_decode_inputs", selected_count = inputs.len(), input_key_sample = ?selected_input_keys, "PostgreSQL contextual decode inputs selected"); std::result::Result::Ok(inputs) }, std::result::Result::Err(error) => { - tracing::error!(target: crate::TRACING_TARGET, action = "list_decode_inputs", error = %error, "PostgreSQL contextual decode input query failed"); + tracing::error!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_decode_inputs", error = %error, "PostgreSQL contextual decode input query failed"); std::result::Result::Err(error) }, }; } -pub(in crate::postgres) async fn list_materialized_events( +pub(crate) async fn list_materialized_events( pool: &sqlx::PgPool, filter: &crate::MaterializedEventFilter, ) -> ks_core::Result> { - tracing::debug!(target: crate::TRACING_TARGET, action = "list_materialized_events", processor_name = ?filter.processor_name, materialized_family = ?filter.materialized_family, signature_contains = ?filter.signature_contains, limit = filter.limit, "query bounded PostgreSQL materialized events"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_materialized_events", processor_name = ?filter.processor_name, materialized_family = ?filter.materialized_family, signature_contains = ?filter.signature_contains, limit = filter.limit, "query bounded PostgreSQL materialized events"); if filter.limit == 0 || filter.limit > crate::MAX_MATERIALIZED_EVENT_QUERY_ROWS { return std::result::Result::Err(ks_core::Error::db(format!( "materialized event query limit must be between 1 and {}", @@ -145,15 +142,15 @@ pub(in crate::postgres) async fn list_materialized_events( updated_at: row.updated_at, }); } - tracing::debug!(target: crate::TRACING_TARGET, action = "list_materialized_events", row_count = output.len(), "bounded PostgreSQL materialized events loaded"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_materialized_events", row_count = output.len(), "bounded PostgreSQL materialized events loaded"); return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn is_decode_current( +pub(crate) async fn is_decode_current( pool: &sqlx::PgPool, identity: &crate::ProcessingLedgerIdentity, ) -> ks_core::Result { - tracing::debug!(target: crate::TRACING_TARGET, action = "is_decode_current", stage = %identity.stage, processor_name = %identity.processor_name, processor_version = %identity.processor_version, input_key = %identity.input_key, input_hash = %identity.input_hash, "query PostgreSQL processing ledger current state"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "is_decode_current", stage = %identity.stage, processor_name = %identity.processor_name, processor_version = %identity.processor_version, input_key = %identity.input_key, input_hash = %identity.input_hash, "query PostgreSQL processing ledger current state"); let query_result = sqlx::query_scalar::( "SELECT EXISTS(SELECT 1 FROM kb_sol_ops_processing_ledger WHERE stage = $1 AND processor_name = $2 AND processor_version = $3 AND input_key = $4 AND input_hash = $5 AND status = 'succeeded')", ) @@ -166,11 +163,11 @@ pub(in crate::postgres) async fn is_decode_current( .await; return match query_result { std::result::Result::Ok(value) => { - tracing::debug!(target: crate::TRACING_TARGET, action = "is_decode_current", stage = %identity.stage, processor_name = %identity.processor_name, processor_version = %identity.processor_version, input_key = %identity.input_key, input_hash = %identity.input_hash, current = value, "PostgreSQL processing ledger current state loaded"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "is_decode_current", stage = %identity.stage, processor_name = %identity.processor_name, processor_version = %identity.processor_version, input_key = %identity.input_key, input_hash = %identity.input_hash, current = value, "PostgreSQL processing ledger current state loaded"); std::result::Result::Ok(value) }, std::result::Result::Err(error) => { - tracing::error!(target: crate::TRACING_TARGET, action = "is_decode_current", stage = %identity.stage, processor_name = %identity.processor_name, processor_version = %identity.processor_version, input_key = %identity.input_key, error = %error, "PostgreSQL processing ledger current check failed"); + tracing::error!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "is_decode_current", stage = %identity.stage, processor_name = %identity.processor_name, processor_version = %identity.processor_version, input_key = %identity.input_key, error = %error, "PostgreSQL processing ledger current check failed"); std::result::Result::Err(ks_core::Error::db(format!( "postgres decode ledger current check failed: {error}" ))) @@ -178,12 +175,12 @@ pub(in crate::postgres) async fn is_decode_current( }; } -pub(in crate::postgres) async fn persist_decode_coverage_declarations( +pub(crate) async fn persist_decode_coverage_declarations( pool: &sqlx::PgPool, declarations: &[crate::DecodeCoverageDeclarationInsert], ) -> ks_core::Result { if declarations.is_empty() { - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_coverage_declarations", declaration_count = 0_usize, "skip empty PostgreSQL decode coverage declarations"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_coverage_declarations", declaration_count = 0_usize, "skip empty PostgreSQL decode coverage declarations"); return std::result::Result::Ok(crate::InsertOutcome::new(0, 0, 0)); } let processor_name = declarations[0].processor_name.as_str(); @@ -192,7 +189,7 @@ pub(in crate::postgres) async fn persist_decode_coverage_declarations( .iter() .map(|entry| return entry.program_id.as_str()) .collect::>(); - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_coverage_declarations", processor_name = %processor_name, processor_version = %processor_version, declaration_count = declarations.len(), program_ids = ?program_ids, "persist PostgreSQL decode coverage declarations"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_coverage_declarations", processor_name = %processor_name, processor_version = %processor_version, declaration_count = declarations.len(), program_ids = ?program_ids, "persist PostgreSQL decode coverage declarations"); if declarations.iter().any(|entry| { return entry.processor_name != processor_name || entry.processor_version != processor_version @@ -351,16 +348,16 @@ pub(in crate::postgres) async fn persist_decode_coverage_declarations( ))); } let outcome = crate::InsertOutcome::new(inserted_count, updated_count, skipped_count); - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_coverage_declarations", processor_name = %processor_name, processor_version = %processor_version, outcome = ?outcome, "PostgreSQL decode coverage declarations persisted"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_coverage_declarations", processor_name = %processor_name, processor_version = %processor_version, outcome = ?outcome, "PostgreSQL decode coverage declarations persisted"); return std::result::Result::Ok(outcome); } -pub(in crate::postgres) async fn persist_decode_result( +pub(crate) async fn persist_decode_result( pool: &sqlx::PgPool, bundle: &crate::DecodePersistenceBundle, force_replay: bool, ) -> ks_core::Result { - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_decode_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, stage = %bundle.ledger_identity.stage, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, input_key = %bundle.ledger_identity.input_key, input_hash = %bundle.ledger_identity.input_hash, status = %bundle.status, observation_count = bundle.observations.len(), coverage_program_id = %bundle.coverage.program_id, force_replay, "persist PostgreSQL contextual decode result"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_decode_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, stage = %bundle.ledger_identity.stage, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, input_key = %bundle.ledger_identity.input_key, input_hash = %bundle.ledger_identity.input_hash, status = %bundle.status, observation_count = bundle.observations.len(), coverage_program_id = %bundle.coverage.program_id, force_replay, "persist PostgreSQL contextual decode result"); let validation_result = bundle.validate(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); @@ -513,15 +510,15 @@ pub(in crate::postgres) async fn persist_decode_result( }, }; let outcome = crate::InsertOutcome::new(count, 1, 0); - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_decode_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, status = %bundle.status, outcome = ?outcome, committed = true, "PostgreSQL contextual decode result persisted"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_decode_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, status = %bundle.status, outcome = ?outcome, committed = true, "PostgreSQL contextual decode result persisted"); return std::result::Result::Ok(outcome); } -pub(in crate::postgres) async fn mark_decode_failed( +pub(crate) async fn mark_decode_failed( pool: &sqlx::PgPool, failure: &crate::DecodeFailure, ) -> ks_core::Result { - tracing::error!(target: crate::TRACING_TARGET, action = "mark_decode_failed", signature = %failure.signature, instruction_path = %failure.instruction_path, processor_name = %failure.ledger_identity.processor_name, processor_version = %failure.ledger_identity.processor_version, input_key = %failure.ledger_identity.input_key, input_hash = %failure.ledger_identity.input_hash, error_code = %failure.error_code, error_message = %failure.error_message, "persist PostgreSQL contextual decode failure"); + tracing::error!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "mark_decode_failed", signature = %failure.signature, instruction_path = %failure.instruction_path, processor_name = %failure.ledger_identity.processor_name, processor_version = %failure.ledger_identity.processor_version, input_key = %failure.ledger_identity.input_key, input_hash = %failure.ledger_identity.input_hash, error_code = %failure.error_code, error_message = %failure.error_message, "persist PostgreSQL contextual decode failure"); if failure.ledger_identity.stage != "instruction_decode" || failure.signature.trim().is_empty() || failure.instruction_path.trim().is_empty() @@ -572,18 +569,18 @@ pub(in crate::postgres) async fn mark_decode_failed( ))); } let outcome = crate::InsertOutcome::new(0, 1, 0); - tracing::debug!(target: crate::TRACING_TARGET, action = "mark_decode_failed", signature = %failure.signature, instruction_path = %failure.instruction_path, processor_name = %failure.ledger_identity.processor_name, processor_version = %failure.ledger_identity.processor_version, outcome = ?outcome, committed = true, "PostgreSQL contextual decode failure persisted"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "mark_decode_failed", signature = %failure.signature, instruction_path = %failure.instruction_path, processor_name = %failure.ledger_identity.processor_name, processor_version = %failure.ledger_identity.processor_version, outcome = ?outcome, committed = true, "PostgreSQL contextual decode failure persisted"); return std::result::Result::Ok(outcome); } -pub(in crate::postgres) async fn persist_materialization_result( +pub(crate) async fn persist_materialization_result( pool: &sqlx::PgPool, bundle: &crate::MaterializationPersistenceBundle, force_replay: bool, ) -> ks_core::Result { if bundle.status == "failed" { tracing::error!( - target: crate::TRACING_TARGET, + target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_materialization_failure", signature = %bundle.signature, instruction_path = %bundle.instruction_path, @@ -598,7 +595,7 @@ pub(in crate::postgres) async fn persist_materialization_result( "persist PostgreSQL materialization failure" ); } - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_materialization_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, stage = %bundle.ledger_identity.stage, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, input_key = %bundle.ledger_identity.input_key, input_hash = %bundle.ledger_identity.input_hash, source_decoder_name = %bundle.source_decoder_name, source_decoder_version = %bundle.source_decoder_version, source_decode_input_key = %bundle.source_decode_input_key, status = %bundle.status, output_count = bundle.outputs.len(), force_replay, "persist PostgreSQL materialization result"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_materialization_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, stage = %bundle.ledger_identity.stage, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, input_key = %bundle.ledger_identity.input_key, input_hash = %bundle.ledger_identity.input_hash, source_decoder_name = %bundle.source_decoder_name, source_decoder_version = %bundle.source_decoder_version, source_decode_input_key = %bundle.source_decode_input_key, status = %bundle.status, output_count = bundle.outputs.len(), force_replay, "persist PostgreSQL materialization result"); let validation_result = bundle.validate(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); @@ -715,17 +712,17 @@ pub(in crate::postgres) async fn persist_materialization_result( }, }; let outcome = crate::InsertOutcome::new(count, 1, 0); - tracing::debug!(target: crate::TRACING_TARGET, action = "persist_materialization_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, status = %bundle.status, outcome = ?outcome, committed = true, "PostgreSQL materialization result persisted"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "persist_materialization_result", signature = %bundle.signature, instruction_path = %bundle.instruction_path, processor_name = %bundle.ledger_identity.processor_name, processor_version = %bundle.ledger_identity.processor_version, status = %bundle.status, outcome = ?outcome, committed = true, "PostgreSQL materialization result persisted"); return std::result::Result::Ok(outcome); } -pub(in crate::postgres) async fn list_decode_coverage_summary( +pub(crate) async fn list_decode_coverage_summary( pool: &sqlx::PgPool, processor_name: std::option::Option<&str>, processor_version: std::option::Option<&str>, limit: u32, ) -> ks_core::Result> { - tracing::debug!(target: crate::TRACING_TARGET, action = "list_decode_coverage_summary", processor_name = ?processor_name, processor_version = ?processor_version, limit, "query PostgreSQL decode coverage summary"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_decode_coverage_summary", processor_name = ?processor_name, processor_version = ?processor_version, limit, "query PostgreSQL decode coverage summary"); if limit == 0 { return std::result::Result::Err(ks_core::Error::db( "decode coverage summary limit must be greater than zero", @@ -756,7 +753,7 @@ pub(in crate::postgres) async fn list_decode_coverage_summary( std::result::Result::Err(error) => return std::result::Result::Err(error), } } - tracing::debug!(target: crate::TRACING_TARGET, action = "list_decode_coverage_summary", processor_name = ?processor_name, processor_version = ?processor_version, row_count = output.len(), "PostgreSQL decode coverage summary loaded"); + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "list_decode_coverage_summary", processor_name = ?processor_name, processor_version = ?processor_version, row_count = output.len(), "PostgreSQL decode coverage summary loaded"); return std::result::Result::Ok(output); } @@ -1035,17 +1032,15 @@ mod tests { }; let pool_result = sqlx::PgPool::connect(url.as_str()).await; let pool = result_or_panic(pool_result); - result_or_panic(crate::postgres::query::raw_queries::apply_raw_store_schema(&pool).await); - result_or_panic(crate::postgres::query::core_queries::apply_core_store_schema(&pool).await); - result_or_panic( - crate::postgres::query::decode_pipeline_queries::apply_decode_store_schema(&pool).await, - ); + result_or_panic(crate::apply_raw_store_schema(&pool).await); + result_or_panic(crate::apply_core_store_schema(&pool).await); + result_or_panic(crate::apply_decode_store_schema(&pool).await); return std::option::Option::Some(pool); } #[tokio::test] async fn optional_postgres_coverage_declarations_report_insert_skip_and_update_from_env() { - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let pool = match test_pool_from_env().await { std::option::Option::Some(value) => value, std::option::Option::None => return, @@ -1062,28 +1057,16 @@ mod tests { historical: false, }; let first = result_or_panic( - crate::postgres::query::decode_pipeline_queries::persist_decode_coverage_declarations( - &pool, - &[declaration.clone()], - ) - .await, + crate::persist_decode_coverage_declarations(&pool, &[declaration.clone()]).await, ); assert_eq!(first, crate::InsertOutcome::new(1, 0, 0)); let second = result_or_panic( - crate::postgres::query::decode_pipeline_queries::persist_decode_coverage_declarations( - &pool, - &[declaration.clone()], - ) - .await, + crate::persist_decode_coverage_declarations(&pool, &[declaration.clone()]).await, ); assert_eq!(second, crate::InsertOutcome::new(0, 0, 1)); declaration.historical = true; let third = result_or_panic( - crate::postgres::query::decode_pipeline_queries::persist_decode_coverage_declarations( - &pool, - &[declaration], - ) - .await, + crate::persist_decode_coverage_declarations(&pool, &[declaration]).await, ); assert_eq!(third, crate::InsertOutcome::new(0, 1, 0)); let cleanup_result = sqlx::query( @@ -1097,7 +1080,7 @@ mod tests { #[tokio::test] async fn optional_postgres_same_version_and_hash_is_current_from_env() { - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let pool = match test_pool_from_env().await { std::option::Option::Some(value) => value, std::option::Option::None => return, @@ -1140,16 +1123,8 @@ mod tests { observations: std::vec::Vec::new(), coverage, }; - result_or_panic( - crate::postgres::query::decode_pipeline_queries::persist_decode_result( - &pool, &bundle, true, - ) - .await, - ); - let current = result_or_panic( - crate::postgres::query::decode_pipeline_queries::is_decode_current(&pool, &identity) - .await, - ); + result_or_panic(crate::persist_decode_result(&pool, &bundle, true).await); + let current = result_or_panic(crate::is_decode_current(&pool, &identity).await); assert!(current); let cleanup_coverage = sqlx::query( "DELETE FROM kb_sol_decode_coverage_observations WHERE processor_name = $1", @@ -1168,7 +1143,7 @@ mod tests { #[tokio::test] async fn optional_postgres_materialized_event_query_is_bounded_and_typed_from_env() { - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let pool = match test_pool_from_env().await { std::option::Option::Some(value) => value, std::option::Option::None => return, @@ -1189,12 +1164,7 @@ mod tests { std::option::Option::Some(input_key.clone()), 1, )); - let rows = result_or_panic( - crate::postgres::query::decode_pipeline_queries::list_materialized_events( - &pool, &filter, - ) - .await, - ); + let rows = result_or_panic(crate::list_materialized_events(&pool, &filter).await); assert_eq!(rows.len(), 1); assert_eq!(rows[0].slot, 42); assert_eq!(rows[0].payload_json["text"], "postgres annotation"); @@ -1211,14 +1181,12 @@ mod tests { std::result::Result::Ok(value) if !value.trim().is_empty() => value, _ => return, }; - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let pool_result = sqlx::PgPool::connect(url.as_str()).await; let pool = result_or_panic(pool_result); - result_or_panic(crate::postgres::query::raw_queries::apply_raw_store_schema(&pool).await); - result_or_panic(crate::postgres::query::core_queries::apply_core_store_schema(&pool).await); - result_or_panic( - crate::postgres::query::decode_pipeline_queries::apply_decode_store_schema(&pool).await, - ); + result_or_panic(crate::apply_raw_store_schema(&pool).await); + result_or_panic(crate::apply_core_store_schema(&pool).await); + result_or_panic(crate::apply_decode_store_schema(&pool).await); execute_sql( &pool, "DELETE FROM kb_sol_decode_events WHERE processor_name = 'decode_atomic_rollback_test'", @@ -1310,11 +1278,7 @@ mod tests { observations: std::vec![observation], coverage, }; - let persistence_result = - crate::postgres::query::decode_pipeline_queries::persist_decode_result( - &pool, &bundle, true, - ) - .await; + let persistence_result = crate::persist_decode_result(&pool, &bundle, true).await; assert!(persistence_result.is_err()); let event_count_result = sqlx::query_scalar::( "SELECT COUNT(*) FROM kb_sol_decode_events WHERE processor_name = 'decode_atomic_rollback_test'", diff --git a/ks-store/src/postgres/query/health_queries.rs b/ks-store/src/postgres/query/health_queries.rs index e71f0e1..2eee2b0 100644 --- a/ks-store/src/postgres/query/health_queries.rs +++ b/ks-store/src/postgres/query/health_queries.rs @@ -1,9 +1,9 @@ // file: ks-store/src/postgres/query/health_queries.rs -// version: 2 +// version: 3 //! PostgreSQL health and diagnostic SQL queries. -pub(in crate::postgres) async fn run_health_check(pool: &sqlx::PgPool) -> ks_core::Result<()> { +pub(crate) async fn run_health_check(pool: &sqlx::PgPool) -> ks_core::Result<()> { let query_result = sqlx::query_scalar::("SELECT 1").fetch_one(pool).await; return match query_result { std::result::Result::Ok(_value) => std::result::Result::Ok(()), @@ -13,7 +13,7 @@ pub(in crate::postgres) async fn run_health_check(pool: &sqlx::PgPool) -> ks_cor }; } -pub(in crate::postgres) async fn load_current_schema( +pub(crate) async fn load_current_schema( pool: &sqlx::PgPool, ) -> ks_core::Result { let query_result = @@ -28,7 +28,7 @@ pub(in crate::postgres) async fn load_current_schema( }; } -pub(in crate::postgres) async fn load_server_version( +pub(crate) async fn load_server_version( pool: &sqlx::PgPool, ) -> ks_core::Result { let query_result = @@ -43,7 +43,7 @@ pub(in crate::postgres) async fn load_server_version( }; } -pub(in crate::postgres) async fn load_migration_table_name( +pub(crate) async fn load_migration_table_name( pool: &sqlx::PgPool, ) -> ks_core::Result> { let query_result = @@ -60,7 +60,7 @@ pub(in crate::postgres) async fn load_migration_table_name( }; } -pub(in crate::postgres) async fn load_latest_migration_version( +pub(crate) async fn load_latest_migration_version( pool: &sqlx::PgPool, ) -> ks_core::Result> { let query_result = sqlx::query_scalar::>( diff --git a/ks-store/src/postgres/query/raw_queries.rs b/ks-store/src/postgres/query/raw_queries.rs index e50fc9a..aaff0d0 100644 --- a/ks-store/src/postgres/query/raw_queries.rs +++ b/ks-store/src/postgres/query/raw_queries.rs @@ -1,11 +1,9 @@ // file: ks-store/src/postgres/query/raw_queries.rs -// version: 4 +// version: 6 //! PostgreSQL canonical transaction and acquisition observation SQL queries. -pub(in crate::postgres) async fn apply_raw_store_schema( - pool: &sqlx::PgPool, -) -> ks_core::Result<()> { +pub(crate) async fn apply_raw_store_schema(pool: &sqlx::PgPool) -> ks_core::Result<()> { let validation_result = crate::validate_raw_store_table_names(); if let std::result::Result::Err(error) = validation_result { return std::result::Result::Err(error); @@ -45,7 +43,7 @@ pub(in crate::postgres) async fn apply_raw_store_schema( return std::result::Result::Ok(()); } -pub(in crate::postgres) async fn has_raw_transaction_signature( +pub(crate) async fn has_raw_transaction_signature( pool: &sqlx::PgPool, signature: &str, ) -> ks_core::Result { @@ -70,7 +68,7 @@ pub(in crate::postgres) async fn has_raw_transaction_signature( }; } -pub(in crate::postgres) async fn has_transaction_observation_key( +pub(crate) async fn has_transaction_observation_key( pool: &sqlx::PgPool, observation_key: &str, ) -> ks_core::Result { @@ -95,7 +93,7 @@ pub(in crate::postgres) async fn has_transaction_observation_key( }; } -pub(in crate::postgres) async fn insert_raw_transaction( +pub(crate) async fn insert_raw_transaction( pool: &sqlx::PgPool, input: &crate::RawTransactionInsert, ) -> ks_core::Result { @@ -136,7 +134,7 @@ pub(in crate::postgres) async fn insert_raw_transaction( }; } -pub(in crate::postgres) async fn insert_transaction_observation( +pub(crate) async fn insert_transaction_observation( pool: &sqlx::PgPool, input: &crate::TransactionObservationInsert, ) -> ks_core::Result { @@ -193,7 +191,7 @@ pub(in crate::postgres) async fn insert_transaction_observation( }; } -pub(in crate::postgres) async fn update_raw_payload_lifecycle( +pub(crate) async fn update_raw_payload_lifecycle( pool: &sqlx::PgPool, mark: &crate::RawPayloadLifecycleMark, ) -> ks_core::Result { @@ -378,7 +376,7 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => return, }; - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false); let options = match options_result { std::result::Result::Ok(value) => value, diff --git a/ks-store/src/postgres/query/replay_candidate_queries.rs b/ks-store/src/postgres/query/replay_candidate_queries.rs index 537f23b..297dd73 100644 --- a/ks-store/src/postgres/query/replay_candidate_queries.rs +++ b/ks-store/src/postgres/query/replay_candidate_queries.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/query/replay_candidate_queries.rs -// version: 4 +// version: 6 //! Read-only PostgreSQL queries for replay candidate discovery. @@ -14,9 +14,9 @@ struct ReplayTransactionCandidateRow { ledger_status: std::string::String, processor_version: std::option::Option, attempt_count: i32, - outer_instruction_count: i64, + top_level_instruction_count: i64, inner_instruction_count: i64, - outer_program_count: i64, + top_level_program_count: i64, inner_program_count: i64, updated_at: std::string::String, } @@ -25,7 +25,7 @@ struct ReplayTransactionCandidateRow { struct ReplayProgramSummaryRow { program_id: std::string::String, transaction_count: i64, - outer_instruction_count: i64, + top_level_instruction_count: i64, inner_instruction_count: i64, log_count: i64, min_slot: i64, @@ -42,10 +42,10 @@ struct ReplayEntitySummaryRow { max_slot: i64, } -pub(in crate::postgres) async fn list_replay_transaction_candidates( +pub(crate) async fn list_replay_transaction_candidates( pool: &sqlx::PgPool, - filter: &crate::PostgresReplayTransactionFilter, -) -> ks_core::Result> { + filter: &crate::ReplayTransactionFilter, +) -> ks_core::Result> { let min_slot_result = crate::postgres::query::replay_candidate_queries::optional_sql_bigint(filter.min_slot); let min_slot = match min_slot_result { @@ -93,7 +93,7 @@ pub(in crate::postgres) async fn list_replay_transaction_candidates( }; let mut output = std::vec::Vec::with_capacity(rows.len()); for row in rows { - output.push(crate::PostgresReplayTransactionCandidate { + output.push(crate::ReplayTransactionCandidate { signature: row.signature, slot: row.slot, raw_processing_state: row.raw_processing_state, @@ -103,9 +103,9 @@ pub(in crate::postgres) async fn list_replay_transaction_candidates( ledger_status: row.ledger_status, processor_version: row.processor_version, attempt_count: row.attempt_count, - outer_instruction_count: row.outer_instruction_count, + top_level_instruction_count: row.top_level_instruction_count, inner_instruction_count: row.inner_instruction_count, - outer_program_count: row.outer_program_count, + top_level_program_count: row.top_level_program_count, inner_program_count: row.inner_program_count, updated_at: row.updated_at, }); @@ -113,13 +113,13 @@ pub(in crate::postgres) async fn list_replay_transaction_candidates( return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn list_replay_program_summaries( +pub(crate) async fn list_replay_program_summaries( pool: &sqlx::PgPool, - filter: &crate::PostgresReplayProgramFilter, -) -> ks_core::Result> { + filter: &crate::ReplayProgramFilter, +) -> ks_core::Result> { let query_result = sqlx::query_as::( r#"WITH occurrences AS ( - SELECT program_id, signature, slot, 'outer'::TEXT AS scope FROM kb_sol_core_instructions + SELECT program_id, signature, slot, 'top_level'::TEXT AS scope FROM kb_sol_core_instructions UNION ALL SELECT program_id, signature, slot, 'inner'::TEXT AS scope FROM kb_sol_core_inner_instructions UNION ALL @@ -127,7 +127,7 @@ pub(in crate::postgres) async fn list_replay_program_summaries( ) SELECT program_id, COUNT(DISTINCT signature)::BIGINT AS transaction_count, - COUNT(*) FILTER (WHERE scope = 'outer')::BIGINT AS outer_instruction_count, + COUNT(*) FILTER (WHERE scope = 'top_level')::BIGINT AS top_level_instruction_count, COUNT(*) FILTER (WHERE scope = 'inner')::BIGINT AS inner_instruction_count, COUNT(*) FILTER (WHERE scope = 'logs')::BIGINT AS log_count, MIN(slot)::BIGINT AS min_slot, @@ -152,10 +152,10 @@ pub(in crate::postgres) async fn list_replay_program_summaries( }; let mut output = std::vec::Vec::with_capacity(rows.len()); for row in rows { - output.push(crate::PostgresReplayProgramSummary { + output.push(crate::ReplayProgramSummary { program_id: row.program_id, transaction_count: row.transaction_count, - outer_instruction_count: row.outer_instruction_count, + top_level_instruction_count: row.top_level_instruction_count, inner_instruction_count: row.inner_instruction_count, log_count: row.log_count, min_slot: row.min_slot, @@ -165,10 +165,10 @@ pub(in crate::postgres) async fn list_replay_program_summaries( return std::result::Result::Ok(output); } -pub(in crate::postgres) async fn list_replay_entity_summaries( +pub(crate) async fn list_replay_entity_summaries( pool: &sqlx::PgPool, - filter: &crate::PostgresReplayEntityFilter, -) -> ks_core::Result> { + filter: &crate::ReplayEntityFilter, +) -> ks_core::Result> { let entity_kind = filter.entity_kind.as_sql(); let query_result = sqlx::query_as::< sqlx::Postgres, @@ -214,7 +214,7 @@ pub(in crate::postgres) async fn list_replay_entity_summaries( }; let mut output = std::vec::Vec::with_capacity(rows.len()); for row in rows { - output.push(crate::PostgresReplayEntitySummary { + output.push(crate::ReplayEntitySummary { entity_kind: row.entity_kind, entity_value: row.entity_value, transaction_count: row.transaction_count, @@ -245,9 +245,9 @@ fn transaction_candidate_sql(order: &str) -> &'static str { COALESCE(ledger.status, 'not_started') AS ledger_status, ledger.processor_version, COALESCE(ledger.attempt_count, 0)::INTEGER AS attempt_count, - COALESCE(outer_stats.instruction_count, 0)::BIGINT AS outer_instruction_count, + COALESCE(top_level_stats.instruction_count, 0)::BIGINT AS top_level_instruction_count, COALESCE(inner_stats.instruction_count, 0)::BIGINT AS inner_instruction_count, - COALESCE(outer_stats.program_count, 0)::BIGINT AS outer_program_count, + COALESCE(top_level_stats.program_count, 0)::BIGINT AS top_level_program_count, COALESCE(inner_stats.program_count, 0)::BIGINT AS inner_program_count, raw.updated_at::TEXT AS updated_at FROM kb_sol_raw_transactions raw @@ -265,7 +265,7 @@ fn transaction_candidate_sql(order: &str) -> &'static str { SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count FROM kb_sol_core_instructions WHERE signature = raw.signature - ) outer_stats ON TRUE + ) top_level_stats ON TRUE LEFT JOIN LATERAL ( SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count FROM kb_sol_core_inner_instructions @@ -278,11 +278,11 @@ fn transaction_candidate_sql(order: &str) -> &'static str { AND ($5::TEXT IS NULL OR ($5 = 'not_started' AND ledger.status IS NULL) OR ledger.status = $5) AND ($6::TEXT IS NULL OR ($7 = 'any' AND ( - EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6) OR + EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_top_level WHERE candidate_top_level.signature = raw.signature AND candidate_top_level.program_id = $6) OR EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6) OR EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6) )) OR - ($7 = 'outer' AND EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6)) OR + ($7 = 'top_level' AND EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_top_level WHERE candidate_top_level.signature = raw.signature AND candidate_top_level.program_id = $6)) OR ($7 = 'inner' AND EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6)) OR ($7 = 'logs' AND EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6))) AND ($8::TEXT IS NULL OR @@ -313,9 +313,9 @@ fn transaction_candidate_sql(order: &str) -> &'static str { COALESCE(ledger.status, 'not_started') AS ledger_status, ledger.processor_version, COALESCE(ledger.attempt_count, 0)::INTEGER AS attempt_count, - COALESCE(outer_stats.instruction_count, 0)::BIGINT AS outer_instruction_count, + COALESCE(top_level_stats.instruction_count, 0)::BIGINT AS top_level_instruction_count, COALESCE(inner_stats.instruction_count, 0)::BIGINT AS inner_instruction_count, - COALESCE(outer_stats.program_count, 0)::BIGINT AS outer_program_count, + COALESCE(top_level_stats.program_count, 0)::BIGINT AS top_level_program_count, COALESCE(inner_stats.program_count, 0)::BIGINT AS inner_program_count, raw.updated_at::TEXT AS updated_at FROM kb_sol_raw_transactions raw @@ -333,7 +333,7 @@ fn transaction_candidate_sql(order: &str) -> &'static str { SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count FROM kb_sol_core_instructions WHERE signature = raw.signature - ) outer_stats ON TRUE + ) top_level_stats ON TRUE LEFT JOIN LATERAL ( SELECT COUNT(*)::BIGINT AS instruction_count, COUNT(DISTINCT program_id)::BIGINT AS program_count FROM kb_sol_core_inner_instructions @@ -346,11 +346,11 @@ fn transaction_candidate_sql(order: &str) -> &'static str { AND ($5::TEXT IS NULL OR ($5 = 'not_started' AND ledger.status IS NULL) OR ledger.status = $5) AND ($6::TEXT IS NULL OR ($7 = 'any' AND ( - EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6) OR + EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_top_level WHERE candidate_top_level.signature = raw.signature AND candidate_top_level.program_id = $6) OR EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6) OR EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6) )) OR - ($7 = 'outer' AND EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_outer WHERE candidate_outer.signature = raw.signature AND candidate_outer.program_id = $6)) OR + ($7 = 'top_level' AND EXISTS (SELECT 1 FROM kb_sol_core_instructions candidate_top_level WHERE candidate_top_level.signature = raw.signature AND candidate_top_level.program_id = $6)) OR ($7 = 'inner' AND EXISTS (SELECT 1 FROM kb_sol_core_inner_instructions candidate_inner WHERE candidate_inner.signature = raw.signature AND candidate_inner.program_id = $6)) OR ($7 = 'logs' AND EXISTS (SELECT 1 FROM kb_sol_core_logs candidate_log WHERE candidate_log.signature = raw.signature AND candidate_log.program_id = $6))) AND ($8::TEXT IS NULL OR @@ -400,7 +400,7 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => return, }; - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false); let options = match options_result { std::result::Result::Ok(value) => value, @@ -419,14 +419,14 @@ mod tests { if let std::result::Result::Err(error) = core_schema_result { panic!("unexpected core schema error: {error}"); } - let transaction_filter_result = crate::PostgresReplayTransactionFilter::new( + let transaction_filter_result = crate::ReplayTransactionFilter::new( std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None, - crate::PostgresReplayProgramScope::Any, + crate::ReplayProgramScope::Any, std::option::Option::None, std::option::Option::None, 10, @@ -442,8 +442,7 @@ mod tests { if let std::result::Result::Err(error) = transaction_result { panic!("unexpected transaction candidate query error: {error}"); } - let program_filter_result = - crate::PostgresReplayProgramFilter::new(std::option::Option::None, 10); + let program_filter_result = crate::ReplayProgramFilter::new(std::option::Option::None, 10); let program_filter = match program_filter_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("unexpected program filter error: {error}"), @@ -453,12 +452,12 @@ mod tests { panic!("unexpected program summary query error: {error}"); } for entity_kind in [ - crate::PostgresReplayEntityKind::Mint, - crate::PostgresReplayEntityKind::Owner, - crate::PostgresReplayEntityKind::AccountKey, + crate::ReplayEntityKind::Mint, + crate::ReplayEntityKind::Owner, + crate::ReplayEntityKind::AccountKey, ] { let entity_filter_result = - crate::PostgresReplayEntityFilter::new(entity_kind, std::option::Option::None, 10); + crate::ReplayEntityFilter::new(entity_kind, std::option::Option::None, 10); let entity_filter = match entity_filter_result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diff --git a/ks-store/src/postgres/query/table_diagnostics_queries.rs b/ks-store/src/postgres/query/table_diagnostics_queries.rs index 05314ca..acd2926 100644 --- a/ks-store/src/postgres/query/table_diagnostics_queries.rs +++ b/ks-store/src/postgres/query/table_diagnostics_queries.rs @@ -1,14 +1,11 @@ // file: ks-store/src/postgres/query/table_diagnostics_queries.rs -// version: 3 +// version: 6 //! Read-only PostgreSQL diagnostics for known Solana store tables. use sqlx::Row; // rust-rules: trait-import -pub(in crate::postgres) async fn table_exists( - pool: &sqlx::PgPool, - table_name: &str, -) -> ks_core::Result { +pub(crate) async fn table_exists(pool: &sqlx::PgPool, table_name: &str) -> ks_core::Result { let query_result = sqlx::query_scalar::("SELECT to_regclass($1)::text IS NOT NULL") .bind(table_name) @@ -22,111 +19,111 @@ pub(in crate::postgres) async fn table_exists( }; } -pub(in crate::postgres) async fn load_table_statistics( +pub(crate) async fn load_table_statistics( pool: &sqlx::PgPool, table_name: &str, -) -> ks_core::Result { +) -> ks_core::Result { return match table_name { crate::RAW_TRANSACTIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_raw_transactions_sql(), + crate::table_stats_kb_sol_raw_transactions_sql(), table_name, ) .await }, crate::TRANSACTION_OBSERVATIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_obs_transaction_observations_sql(), + crate::table_stats_kb_sol_obs_transaction_observations_sql(), table_name, ) .await }, crate::CORE_TRANSACTIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_core_transactions_sql(), + crate::table_stats_kb_sol_core_transactions_sql(), table_name, ) .await }, crate::CORE_ACCOUNT_KEYS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_core_account_keys_sql(), + crate::table_stats_kb_sol_core_account_keys_sql(), table_name, ) .await }, crate::CORE_INSTRUCTIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_core_instructions_sql(), + crate::table_stats_kb_sol_core_instructions_sql(), table_name, ) .await }, crate::CORE_INNER_INSTRUCTIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_core_inner_instructions_sql(), + crate::table_stats_kb_sol_core_inner_instructions_sql(), table_name, ) .await }, crate::CORE_LOGS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_core_logs_sql(), + crate::table_stats_kb_sol_core_logs_sql(), table_name, ) .await }, crate::CORE_BALANCE_CHANGES_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_core_balance_changes_sql(), + crate::table_stats_kb_sol_core_balance_changes_sql(), table_name, ) .await }, crate::PROCESSING_LEDGER_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_ops_processing_ledger_sql(), + crate::table_stats_kb_sol_ops_processing_ledger_sql(), table_name, ) .await }, crate::DECODE_EVENTS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_decode_events_sql(), + crate::table_stats_kb_sol_decode_events_sql(), table_name, ) .await }, crate::DECODE_COVERAGE_DECLARATIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_decode_coverage_declarations_sql(), + crate::table_stats_kb_sol_decode_coverage_declarations_sql(), table_name, ) .await }, crate::DECODE_COVERAGE_OBSERVATIONS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_decode_coverage_observations_sql(), + crate::table_stats_kb_sol_decode_coverage_observations_sql(), table_name, ) .await }, crate::MATERIALIZED_EVENTS_TABLE_NAME => { - crate::postgres::query::table_diagnostics_queries::load_table_statistics_from_sql( + load_table_statistics_from_sql( pool, - crate::postgres::migrations::table_stats_kb_sol_mat_events_sql(), + crate::table_stats_kb_sol_mat_events_sql(), table_name, ) .await @@ -141,7 +138,7 @@ async fn load_table_statistics_from_sql( pool: &sqlx::PgPool, sql: &'static str, table_name: &str, -) -> ks_core::Result { +) -> ks_core::Result { let query_result = sqlx::query(sql).fetch_one(pool).await; let row = match query_result { std::result::Result::Ok(value) => value, @@ -188,8 +185,8 @@ async fn load_table_statistics_from_sql( ))); }, }; - return std::result::Result::Ok(crate::PostgresTableStatistics { - row_count, + return std::result::Result::Ok(crate::StoreResourceStatistics { + record_count: row_count, min_slot, max_slot, latest_created_at, diff --git a/ks-store/src/postgres/repository/core_extraction_repository.rs b/ks-store/src/postgres/repository/core_extraction_repository.rs index 950babb..7eabac6 100644 --- a/ks-store/src/postgres/repository/core_extraction_repository.rs +++ b/ks-store/src/postgres/repository/core_extraction_repository.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/repository/core_extraction_repository.rs -// version: 2 +// version: 3 //! PostgreSQL atomic canonical transaction to core extraction repository. @@ -13,11 +13,7 @@ impl crate::CoreExtractionStore for crate::PostgresStore { &self, filter: &crate::CoreExtractionSelectionFilter, ) -> ks_core::Result> { - return crate::postgres::query::list_raw_transactions_for_core_extraction( - self.pool(), - filter, - ) - .await; + return crate::list_raw_transactions_for_core_extraction(self.pool(), filter).await; } #[expect( @@ -28,7 +24,7 @@ impl crate::CoreExtractionStore for crate::PostgresStore { &self, identity: &crate::ProcessingLedgerIdentity, ) -> ks_core::Result { - return crate::postgres::query::is_core_extraction_current(self.pool(), identity).await; + return crate::is_core_extraction_current(self.pool(), identity).await; } #[expect( @@ -40,8 +36,7 @@ impl crate::CoreExtractionStore for crate::PostgresStore { bundle: &crate::CoreExtractionBundle, force_replay: bool, ) -> ks_core::Result { - return crate::postgres::query::persist_core_extraction(self.pool(), bundle, force_replay) - .await; + return crate::persist_core_extraction(self.pool(), bundle, force_replay).await; } #[expect( @@ -52,6 +47,6 @@ impl crate::CoreExtractionStore for crate::PostgresStore { &self, failure: &crate::CoreExtractionFailure, ) -> ks_core::Result { - return crate::postgres::query::mark_core_extraction_failed(self.pool(), failure).await; + return crate::mark_core_extraction_failed(self.pool(), failure).await; } } diff --git a/ks-store/src/postgres/repository/core_transaction_repository.rs b/ks-store/src/postgres/repository/core_transaction_repository.rs index b4a9681..7054484 100644 --- a/ks-store/src/postgres/repository/core_transaction_repository.rs +++ b/ks-store/src/postgres/repository/core_transaction_repository.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/repository/core_transaction_repository.rs -// version: 2 +// version: 3 //! PostgreSQL core Solana repository implementation. @@ -13,7 +13,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, input: &crate::CoreTransactionInsert, ) -> ks_core::Result { - return crate::postgres::query::insert_core_transaction(self.pool(), input).await; + return crate::insert_core_transaction(self.pool(), input).await; } #[expect( @@ -24,7 +24,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, inputs: &[crate::CoreAccountKeyInsert], ) -> ks_core::Result { - return crate::postgres::query::insert_core_account_keys(self.pool(), inputs).await; + return crate::insert_core_account_keys(self.pool(), inputs).await; } #[expect( @@ -35,7 +35,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, inputs: &[crate::CoreInstructionInsert], ) -> ks_core::Result { - return crate::postgres::query::insert_core_instructions(self.pool(), inputs).await; + return crate::insert_core_instructions(self.pool(), inputs).await; } #[expect( @@ -46,7 +46,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, inputs: &[crate::CoreInnerInstructionInsert], ) -> ks_core::Result { - return crate::postgres::query::insert_core_inner_instructions(self.pool(), inputs).await; + return crate::insert_core_inner_instructions(self.pool(), inputs).await; } #[expect( @@ -57,7 +57,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, inputs: &[crate::CoreLogInsert], ) -> ks_core::Result { - return crate::postgres::query::insert_core_logs(self.pool(), inputs).await; + return crate::insert_core_logs(self.pool(), inputs).await; } #[expect( @@ -68,7 +68,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, inputs: &[crate::CoreBalanceChangeInsert], ) -> ks_core::Result { - return crate::postgres::query::insert_core_balance_changes(self.pool(), inputs).await; + return crate::insert_core_balance_changes(self.pool(), inputs).await; } #[expect( @@ -80,12 +80,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { filter: &crate::CoreInstructionReplayFilter, page_request: &crate::PageRequest, ) -> ks_core::Result> { - return crate::postgres::query::list_core_instructions_for_replay( - self.pool(), - filter, - page_request, - ) - .await; + return crate::list_core_instructions_for_replay(self.pool(), filter, page_request).await; } #[expect( @@ -97,12 +92,7 @@ impl crate::CoreTransactionStore for crate::PostgresStore { filter: &crate::CoreInstructionReplayFilter, page_request: &crate::PageRequest, ) -> ks_core::Result> { - return crate::postgres::query::list_core_instruction_replay_inputs( - self.pool(), - filter, - page_request, - ) - .await; + return crate::list_core_instruction_replay_inputs(self.pool(), filter, page_request).await; } #[expect( @@ -113,6 +103,6 @@ impl crate::CoreTransactionStore for crate::PostgresStore { &self, mark: &crate::CoreInstructionLifecycleMark, ) -> ks_core::Result { - return crate::postgres::query::update_core_instruction_lifecycle(self.pool(), mark).await; + return crate::update_core_instruction_lifecycle(self.pool(), mark).await; } } diff --git a/ks-store/src/postgres/repository/decode_pipeline_repository.rs b/ks-store/src/postgres/repository/decode_pipeline_repository.rs index a54bed9..5b5bdf7 100644 --- a/ks-store/src/postgres/repository/decode_pipeline_repository.rs +++ b/ks-store/src/postgres/repository/decode_pipeline_repository.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/repository/decode_pipeline_repository.rs -// version: 2 +// version: 3 //! PostgreSQL contextual decode and materialization repository. @@ -13,7 +13,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { &self, filter: &crate::DecodeSelectionFilter, ) -> ks_core::Result> { - return crate::postgres::query::list_decode_inputs(self.pool(), filter).await; + return crate::list_decode_inputs(self.pool(), filter).await; } #[expect( @@ -24,7 +24,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { &self, identity: &crate::ProcessingLedgerIdentity, ) -> ks_core::Result { - return crate::postgres::query::is_decode_current(self.pool(), identity).await; + return crate::is_decode_current(self.pool(), identity).await; } #[expect( @@ -35,11 +35,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { &self, declarations: &[crate::DecodeCoverageDeclarationInsert], ) -> ks_core::Result { - return crate::postgres::query::persist_decode_coverage_declarations( - self.pool(), - declarations, - ) - .await; + return crate::persist_decode_coverage_declarations(self.pool(), declarations).await; } #[expect( @@ -51,8 +47,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { bundle: &crate::DecodePersistenceBundle, force_replay: bool, ) -> ks_core::Result { - return crate::postgres::query::persist_decode_result(self.pool(), bundle, force_replay) - .await; + return crate::persist_decode_result(self.pool(), bundle, force_replay).await; } #[expect( @@ -63,7 +58,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { &self, failure: &crate::DecodeFailure, ) -> ks_core::Result { - return crate::postgres::query::mark_decode_failed(self.pool(), failure).await; + return crate::mark_decode_failed(self.pool(), failure).await; } #[expect( @@ -75,12 +70,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { bundle: &crate::MaterializationPersistenceBundle, force_replay: bool, ) -> ks_core::Result { - return crate::postgres::query::persist_materialization_result( - self.pool(), - bundle, - force_replay, - ) - .await; + return crate::persist_materialization_result(self.pool(), bundle, force_replay).await; } #[expect( @@ -93,7 +83,7 @@ impl crate::DecodePipelineStore for crate::PostgresStore { processor_version: std::option::Option<&str>, limit: u32, ) -> ks_core::Result> { - return crate::postgres::query::list_decode_coverage_summary( + return crate::list_decode_coverage_summary( self.pool(), processor_name, processor_version, @@ -110,6 +100,6 @@ impl crate::DecodePipelineStore for crate::PostgresStore { &self, filter: &crate::MaterializedEventFilter, ) -> ks_core::Result> { - return crate::postgres::query::list_materialized_events(self.pool(), filter).await; + return crate::list_materialized_events(self.pool(), filter).await; } } diff --git a/ks-store/src/postgres/repository/raw_transaction_repository.rs b/ks-store/src/postgres/repository/raw_transaction_repository.rs index f735ea3..88776a9 100644 --- a/ks-store/src/postgres/repository/raw_transaction_repository.rs +++ b/ks-store/src/postgres/repository/raw_transaction_repository.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/repository/raw_transaction_repository.rs -// version: 2 +// version: 3 //! PostgreSQL canonical transaction and acquisition observation repository implementation. @@ -13,11 +13,7 @@ impl crate::RawTransactionStore for crate::PostgresStore { &self, signature: &ks_lib::MdSignature, ) -> ks_core::Result { - return crate::postgres::query::has_raw_transaction_signature( - self.pool(), - signature.0.as_str(), - ) - .await; + return crate::has_raw_transaction_signature(self.pool(), signature.0.as_str()).await; } #[expect( @@ -28,11 +24,7 @@ impl crate::RawTransactionStore for crate::PostgresStore { &self, observation_key: &str, ) -> ks_core::Result { - return crate::postgres::query::has_transaction_observation_key( - self.pool(), - observation_key, - ) - .await; + return crate::has_transaction_observation_key(self.pool(), observation_key).await; } #[expect( @@ -43,7 +35,7 @@ impl crate::RawTransactionStore for crate::PostgresStore { &self, input: &crate::RawTransactionInsert, ) -> ks_core::Result { - return crate::postgres::query::insert_raw_transaction(self.pool(), input).await; + return crate::insert_raw_transaction(self.pool(), input).await; } #[expect( @@ -54,7 +46,7 @@ impl crate::RawTransactionStore for crate::PostgresStore { &self, input: &crate::TransactionObservationInsert, ) -> ks_core::Result { - return crate::postgres::query::insert_transaction_observation(self.pool(), input).await; + return crate::insert_transaction_observation(self.pool(), input).await; } #[expect( @@ -65,6 +57,6 @@ impl crate::RawTransactionStore for crate::PostgresStore { &self, mark: &crate::RawPayloadLifecycleMark, ) -> ks_core::Result { - return crate::postgres::query::update_raw_payload_lifecycle(self.pool(), mark).await; + return crate::update_raw_payload_lifecycle(self.pool(), mark).await; } } diff --git a/ks-store/src/postgres/store.rs b/ks-store/src/postgres/store.rs index 0000ef8..770f79e 100644 --- a/ks-store/src/postgres/store.rs +++ b/ks-store/src/postgres/store.rs @@ -1,42 +1,131 @@ // file: ks-store/src/postgres/store.rs -// version: 4 +// version: 9 -//! Store implementation scaffold for the `ks-store` crate. +//! PostgreSQL store implementation kept behind the backend-agnostic `Store` facade. -/// PostgreSQL store connection options. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PostgresStoreOptions { - /// Database URL or DSN. - pub database_url: std::string::String, - /// Maximum connection count. - pub max_connections: u32, - /// Connection timeout in milliseconds. - pub connect_timeout_ms: u64, - /// Enables idempotent raw schema initialization at startup. - pub auto_initialize_schema: bool, +/// PostgreSQL store connection options interpreted only inside `ks-store`. +#[derive(Clone, Eq, PartialEq)] +pub(crate) struct PostgresStoreOptions { + database_url: std::string::String, + max_connections: u32, + connect_timeout_ms: u64, + auto_initialize_schema: bool, + option_count: u32, } -impl PostgresStoreOptions { - /// Creates validated PostgreSQL store options. - pub fn new( +impl crate::PostgresStoreOptions { + /// Creates validated PostgreSQL store options for crate-internal tests and adapters. + #[cfg(test)] + pub(crate) fn new( database_url: impl std::convert::Into, max_connections: u32, connect_timeout_ms: u64, auto_initialize_schema: bool, + ) -> ks_core::Result { + return Self::new_with_option_count( + database_url, + max_connections, + connect_timeout_ms, + auto_initialize_schema, + 4, + ); + } + + /// Interprets the selected opaque backend options supplied to `Store::open`. + pub(crate) fn from_backend_options(options: &serde_json::Value) -> ks_core::Result { + let object = match options.as_object() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_invalid", + "postgres backend options must be a JSON object", + )); + }, + }; + let database_url = match object.get("url").and_then(serde_json::Value::as_str) { + std::option::Option::Some(value) => value.to_string(), + std::option::Option::None => { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_incomplete", + "postgres backend requires a non-empty connection URL", + )); + }, + }; + let max_connections_u64 = + match object.get("max_connections").and_then(serde_json::Value::as_u64) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_incomplete", + "postgres backend requires max_connections greater than zero", + )); + }, + }; + let max_connections = match u32::try_from(max_connections_u64) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_invalid", + "postgres max_connections does not fit into u32", + )); + }, + }; + let connect_timeout_ms = + match object.get("connect_timeout_ms").and_then(serde_json::Value::as_u64) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_incomplete", + "postgres backend requires connect_timeout_ms greater than zero", + )); + }, + }; + let auto_initialize_schema = + match object.get("auto_initialize_schema").and_then(serde_json::Value::as_bool) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_incomplete", + "postgres backend requires auto_initialize_schema", + )); + }, + }; + let option_count = match u32::try_from(object.len()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => u32::MAX, + }; + return Self::new_with_option_count( + database_url, + max_connections, + connect_timeout_ms, + auto_initialize_schema, + option_count, + ); + } + + fn new_with_option_count( + database_url: impl std::convert::Into, + max_connections: u32, + connect_timeout_ms: u64, + auto_initialize_schema: bool, + option_count: u32, ) -> ks_core::Result { let database_url_value = database_url.into(); if database_url_value.trim().is_empty() { - return std::result::Result::Err(ks_core::Error::db( - "postgres database url must not be empty", + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_incomplete", + "postgres backend requires a non-empty connection URL", )); } if max_connections == 0 { - return std::result::Result::Err(ks_core::Error::db( + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_invalid", "postgres max_connections must be greater than zero", )); } if connect_timeout_ms == 0 { - return std::result::Result::Err(ks_core::Error::db( + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_invalid", "postgres connect_timeout_ms must be greater than zero", )); } @@ -45,173 +134,142 @@ impl PostgresStoreOptions { max_connections, connect_timeout_ms, auto_initialize_schema, + option_count, }); } + /// Returns whether automatic schema initialization is enabled. + pub(crate) fn auto_initialize_schema(&self) -> bool { + return self.auto_initialize_schema; + } + + /// Returns a backend-neutral safe configuration summary. + pub(crate) fn configuration_summary(&self) -> crate::StoreConfigurationSummary { + return crate::StoreConfigurationSummary { + enabled: true, + backend_code: "postgres".to_string(), + connection_configured: !self.database_url.trim().is_empty(), + auto_initialize_schema: self.auto_initialize_schema, + backend_option_count: self.option_count, + }; + } + /// Returns a DSN masked for diagnostics. - pub fn masked_dsn(&self) -> std::string::String { - return crate::mask_postgres_dsn(self.database_url.as_str()); + pub(crate) fn masked_connection_descriptor(&self) -> std::string::String { + return mask_postgres_dsn(self.database_url.as_str()); } } -/// PostgreSQL diagnostic snapshot. -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct PostgresBackendDiagnostics { - /// Backend descriptor safe for UI display. - pub descriptor: crate::StoreBackendDescriptor, - /// Backend health snapshot. - pub health: crate::StoreHealthSnapshot, - /// Migration status snapshot. - pub migrations: crate::StoreMigrationSnapshot, - /// Full PostgreSQL server version string when available. - pub server_version: std::option::Option, -} - -/// Read-only statistics for one PostgreSQL table. -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct PostgresTableStatistics { - /// Number of rows currently stored in the table. - pub row_count: i64, - /// Lowest observed Solana slot when the table contains a slot column and rows. - pub min_slot: std::option::Option, - /// Highest observed Solana slot when the table contains a slot column and rows. - pub max_slot: std::option::Option, - /// Latest insertion timestamp rendered by PostgreSQL for UI diagnostics. - pub latest_created_at: std::option::Option, -} - -/// Read-only diagnostics for one expected PostgreSQL table. -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct PostgresTableDiagnostics { - /// Expected table name. - pub table_name: std::string::String, - /// Logical Solana domain encoded in the table name. - pub domain: std::string::String, - /// Human-readable role of the table. - pub role: std::string::String, - /// Whether the table exists in the current PostgreSQL search path. - pub exists: bool, - /// Table statistics when the table exists. - pub statistics: std::option::Option, -} - -/// PostgreSQL store handle. -#[derive(Clone, Debug)] -pub struct PostgresStore { +/// PostgreSQL store handle kept private to `ks-store`. +#[derive(Clone)] +pub(crate) struct PostgresStore { options: crate::PostgresStoreOptions, pool: sqlx::PgPool, } -impl PostgresStore { - /// Connects to PostgreSQL from typed store options. - pub async fn connect(options: crate::PostgresStoreOptions) -> ks_core::Result { +impl crate::PostgresStore { + /// Connects to PostgreSQL from validated backend options. + pub(crate) async fn connect(options: crate::PostgresStoreOptions) -> ks_core::Result { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", "open PostgreSQL store connection"); let pool_options = sqlx::postgres::PgPoolOptions::new() .max_connections(options.max_connections) .acquire_timeout(std::time::Duration::from_millis(options.connect_timeout_ms)); let connect_result = pool_options.connect(options.database_url.as_str()).await; return match connect_result { std::result::Result::Ok(pool) => { - let store = Self { options, pool }; - if store.options.auto_initialize_schema { - let schema_result = store.initialize_store_schema().await; - if let std::result::Result::Err(error) = schema_result { - return std::result::Result::Err(error); - } - } - std::result::Result::Ok(store) + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", connected = true, "PostgreSQL store connection opened"); + std::result::Result::Ok(Self { options, pool }) + }, + std::result::Result::Err(_error) => { + tracing::error!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "connection_open", connected = false, "PostgreSQL store connection failed"); + std::result::Result::Err(crate::storage_contract_error( + "store_backend_connection_failed", + "postgres connection failed", + )) }, - std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::db( - format!("postgres connection failed: {error}"), - )), }; } - /// Creates a store handle from an existing PostgreSQL pool. - pub fn from_pool(options: crate::PostgresStoreOptions, pool: sqlx::PgPool) -> Self { - return Self { options, pool }; - } - - /// Returns the underlying PostgreSQL pool. - pub fn pool(&self) -> &sqlx::PgPool { + /// Returns the underlying PostgreSQL pool to backend-private repositories. + pub(crate) fn pool(&self) -> &sqlx::PgPool { return &self.pool; } - /// Returns the connection options used to create this store. - pub fn options(&self) -> &crate::PostgresStoreOptions { - return &self.options; - } - /// Applies each idempotent store schema once per invocation in dependency order. - pub async fn initialize_store_schema(&self) -> ks_core::Result<()> { - let raw_result = crate::postgres::query::apply_raw_store_schema(&self.pool).await; + pub(crate) async fn initialize_store_schema(&self) -> ks_core::Result<()> { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "schema_initialize", "initialize PostgreSQL store schema"); + let raw_result = crate::apply_raw_store_schema(&self.pool).await; if let std::result::Result::Err(error) = raw_result { return std::result::Result::Err(error); } - let core_result = crate::postgres::query::apply_core_store_schema(&self.pool).await; + let core_result = crate::apply_core_store_schema(&self.pool).await; if let std::result::Result::Err(error) = core_result { return std::result::Result::Err(error); } - return crate::postgres::query::apply_decode_store_schema(&self.pool).await; + let result = crate::apply_decode_store_schema(&self.pool).await; + if result.is_ok() { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "schema_initialize", initialized = true, "PostgreSQL store schema initialized"); + } + return result; } /// Applies the idempotent minimal raw Solana store schema. - pub async fn initialize_raw_store_schema(&self) -> ks_core::Result<()> { - return crate::postgres::query::apply_raw_store_schema(&self.pool).await; + #[cfg(test)] + pub(crate) async fn initialize_raw_store_schema(&self) -> ks_core::Result<()> { + return crate::apply_raw_store_schema(&self.pool).await; } - /// Applies the idempotent minimal core Solana store schema. - pub async fn initialize_core_store_schema(&self) -> ks_core::Result<()> { + /// Applies the idempotent minimal Core Solana store schema. + #[cfg(test)] + pub(crate) async fn initialize_core_store_schema(&self) -> ks_core::Result<()> { let raw_result = self.initialize_raw_store_schema().await; if let std::result::Result::Err(error) = raw_result { return std::result::Result::Err(error); } - return crate::postgres::query::apply_core_store_schema(&self.pool).await; - } - - /// Applies the idempotent common decode and materialization store schema. - pub async fn initialize_decode_store_schema(&self) -> ks_core::Result<()> { - let core_result = self.initialize_core_store_schema().await; - if let std::result::Result::Err(error) = core_result { - return std::result::Result::Err(error); - } - return crate::postgres::query::apply_decode_store_schema(&self.pool).await; + return crate::apply_core_store_schema(&self.pool).await; } /// Reads a UI-safe backend descriptor. - pub async fn backend_descriptor(&self) -> ks_core::Result { - let schema_result = crate::postgres::query::load_current_schema(&self.pool).await; + pub(crate) async fn backend_descriptor( + &self, + ) -> ks_core::Result { + let schema_result = crate::load_current_schema(&self.pool).await; return match schema_result { std::result::Result::Ok(schema) => crate::StoreBackendDescriptor::new( - crate::StoreBackendKind::Postgres, "postgres", - std::option::Option::Some(self.options.masked_dsn()), + "PostgreSQL", + std::option::Option::Some(self.options.masked_connection_descriptor()), std::option::Option::Some(schema), ), std::result::Result::Err(error) => std::result::Result::Err(error), }; } - /// Reads a PostgreSQL health snapshot. - pub async fn health_snapshot(&self) -> ks_core::Result { - let health_result = crate::postgres::query::run_health_check(&self.pool).await; + /// Reads a PostgreSQL health snapshot without exposing backend error details. + pub(crate) async fn health_snapshot(&self) -> ks_core::Result { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "health_check", "run PostgreSQL store health check"); + let health_result = crate::run_health_check(&self.pool).await; return match health_result { std::result::Result::Ok(()) => crate::StoreHealthSnapshot::new( "postgres", crate::StoreHealthStatus::Healthy, - std::option::Option::Some(std::string::String::from("SELECT 1 succeeded")), + std::option::Option::Some(std::string::String::from( + "backend health check succeeded", + )), ), - std::result::Result::Err(error) => crate::StoreHealthSnapshot::new( + std::result::Result::Err(_error) => crate::StoreHealthSnapshot::new( "postgres", crate::StoreHealthStatus::Unhealthy, - std::option::Option::Some(error.to_string()), + std::option::Option::Some(std::string::String::from("backend health check failed")), ), }; } /// Reads a non-destructive migration snapshot. - pub async fn migration_snapshot(&self) -> ks_core::Result { - let migration_table_result = - crate::postgres::query::load_migration_table_name(&self.pool).await; + pub(crate) async fn migration_snapshot( + &self, + ) -> ks_core::Result { + let migration_table_result = crate::load_migration_table_name(&self.pool).await; return match migration_table_result { std::result::Result::Ok(std::option::Option::None) => { std::result::Result::Ok(crate::StoreMigrationSnapshot::new( @@ -219,7 +277,7 @@ impl PostgresStore { std::option::Option::None, std::vec::Vec::new(), std::option::Option::Some(std::string::String::from( - "no sqlx migration table detected; 0.3.1 canonical acquisition/core schemas use idempotent crate-managed DDL", + "no migration history table detected; crate-managed schema initialization is active", )), )) }, @@ -230,132 +288,121 @@ impl PostgresStore { }; } - /// Reads a complete PostgreSQL diagnostic snapshot. - pub async fn backend_diagnostics(&self) -> ks_core::Result { - let descriptor_result = self.backend_descriptor().await; - let descriptor = match descriptor_result { + /// Reads a complete backend-neutral diagnostic snapshot. + pub(crate) async fn backend_diagnostics( + &self, + ) -> ks_core::Result { + let descriptor = match self.backend_descriptor().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let health_result = self.health_snapshot().await; - let health = match health_result { + let health = match self.health_snapshot().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let migrations_result = self.migration_snapshot().await; - let migrations = match migrations_result { + let migrations = match self.migration_snapshot().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let server_version = match crate::postgres::query::load_server_version(&self.pool).await { + let backend_version = match crate::load_server_version(&self.pool).await { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(_error) => std::option::Option::None, }; - return std::result::Result::Ok(crate::PostgresBackendDiagnostics { + return std::result::Result::Ok(crate::StoreBackendDiagnostics { descriptor, health, migrations, - server_version, + backend_version, }); } - /// Lists bounded raw transaction candidates enriched with core and ledger diagnostics. - pub async fn replay_transaction_candidates( + /// Lists bounded raw transaction candidates enriched with Core and ledger diagnostics. + pub(crate) async fn replay_transaction_candidates( &self, - filter: &crate::PostgresReplayTransactionFilter, - ) -> ks_core::Result> { - return crate::postgres::query::list_replay_transaction_candidates(&self.pool, filter) - .await; + filter: &crate::ReplayTransactionFilter, + ) -> ks_core::Result> { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_transaction_candidates", "query PostgreSQL replay transaction candidates"); + return crate::list_replay_transaction_candidates(&self.pool, filter).await; } - /// Lists bounded program summaries across outer, inner and reliably linked logs. - pub async fn replay_program_summaries( + /// Lists bounded program summaries across top-level, inner and reliably linked logs. + pub(crate) async fn replay_program_summaries( &self, - filter: &crate::PostgresReplayProgramFilter, - ) -> ks_core::Result> { - return crate::postgres::query::list_replay_program_summaries(&self.pool, filter).await; + filter: &crate::ReplayProgramFilter, + ) -> ks_core::Result> { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_program_summaries", "query PostgreSQL replay program summaries"); + return crate::list_replay_program_summaries(&self.pool, filter).await; } - /// Lists bounded mint, owner or account-key summaries from core tables. - pub async fn replay_entity_summaries( + /// Lists bounded mint, owner or account-key summaries from Core facts. + pub(crate) async fn replay_entity_summaries( &self, - filter: &crate::PostgresReplayEntityFilter, - ) -> ks_core::Result> { - return crate::postgres::query::list_replay_entity_summaries(&self.pool, filter).await; + filter: &crate::ReplayEntityFilter, + ) -> ks_core::Result> { + tracing::debug!(target: crate::TRACING_TARGET, backend = "postgres", domain = "ks-store.pg", action = "replay_entity_summaries", "query PostgreSQL replay entity summaries"); + return crate::list_replay_entity_summaries(&self.pool, filter).await; } - /// Reads diagnostics for raw Solana store tables without changing the schema. - pub async fn raw_table_diagnostics( + /// Reads diagnostics for raw store resources without changing the schema. + pub(crate) async fn raw_resource_diagnostics( &self, - ) -> ks_core::Result> { + ) -> ks_core::Result> { let specs = crate::raw_store_table_diagnostic_specs(); - return self.table_diagnostics(&specs).await; + return self.resource_diagnostics(&specs).await; } - /// Reads diagnostics for core Solana store tables without changing the schema. - pub async fn core_table_diagnostics( + /// Reads diagnostics for Core and processing store resources without changing the schema. + pub(crate) async fn core_resource_diagnostics( &self, - ) -> ks_core::Result> { + ) -> ks_core::Result> { let specs = crate::core_store_table_diagnostic_specs(); - return self.table_diagnostics(&specs).await; + return self.resource_diagnostics(&specs).await; } - /// Reads diagnostics for decode and materialization store tables without changing the schema. - pub async fn decode_table_diagnostics( + /// Reads diagnostics for decode and materialization resources without changing the schema. + pub(crate) async fn decode_resource_diagnostics( &self, - ) -> ks_core::Result> { + ) -> ks_core::Result> { let specs = crate::decode_store_table_diagnostic_specs(); - return self.table_diagnostics(&specs).await; + return self.resource_diagnostics(&specs).await; } - /// Reads diagnostics for every known raw/core/decode Solana store table. - pub async fn known_table_diagnostics( + /// Reads diagnostics for every known logical store resource. + pub(crate) async fn known_resource_diagnostics( &self, - ) -> ks_core::Result> { + ) -> ks_core::Result> { let mut diagnostics = std::vec::Vec::new(); - let raw_result = self.raw_table_diagnostics().await; - let raw_tables = match raw_result { + let raw_resources = match self.raw_resource_diagnostics().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - for table in raw_tables { - diagnostics.push(table); - } - let core_result = self.core_table_diagnostics().await; - let core_tables = match core_result { + diagnostics.extend(raw_resources); + let core_resources = match self.core_resource_diagnostics().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - for table in core_tables { - diagnostics.push(table); - } - let decode_result = self.decode_table_diagnostics().await; - let decode_tables = match decode_result { + diagnostics.extend(core_resources); + let decode_resources = match self.decode_resource_diagnostics().await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - for table in decode_tables { - diagnostics.push(table); - } + diagnostics.extend(decode_resources); return std::result::Result::Ok(diagnostics); } - async fn table_diagnostics( + async fn resource_diagnostics( &self, specs: &[crate::PostgresTableDiagnosticSpec], - ) -> ks_core::Result> { + ) -> ks_core::Result> { let mut diagnostics = std::vec::Vec::new(); for spec in specs { - let exists_result = - crate::postgres::query::table_exists(&self.pool, spec.table_name).await; - let exists = match exists_result { + let available = match crate::table_exists(&self.pool, spec.table_name).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let statistics = if exists { + let statistics = if available { let statistics_result = - crate::postgres::query::load_table_statistics(&self.pool, spec.table_name) - .await; + crate::load_table_statistics(&self.pool, spec.table_name).await; match statistics_result { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -363,11 +410,11 @@ impl PostgresStore { } else { std::option::Option::None }; - diagnostics.push(crate::PostgresTableDiagnostics { - table_name: spec.table_name.to_string(), - domain: spec.domain.to_string(), + diagnostics.push(crate::StoreResourceDiagnostics { + resource_code: spec.resource_code.to_string(), + model_code: spec.model_code.to_string(), role: spec.role.to_string(), - exists, + available, statistics, }); } @@ -377,8 +424,7 @@ impl PostgresStore { async fn migration_snapshot_from_existing_table( &self, ) -> ks_core::Result { - let version_result = - crate::postgres::query::load_latest_migration_version(&self.pool).await; + let version_result = crate::load_latest_migration_version(&self.pool).await; return match version_result { std::result::Result::Ok(current_version) => { std::result::Result::Ok(crate::StoreMigrationSnapshot::new( @@ -386,7 +432,7 @@ impl PostgresStore { current_version, std::vec::Vec::new(), std::option::Option::Some(std::string::String::from( - "sqlx migration table detected; canonical acquisition/core schema remains idempotent and crate-managed in 0.3.1", + "migration history table detected", )), )) }, @@ -395,22 +441,18 @@ impl PostgresStore { } } -/// Returns a DSN masked for logs and UI diagnostics. -pub fn mask_postgres_dsn(dsn: &str) -> std::string::String { +/// Returns a PostgreSQL connection descriptor masked for logs and diagnostics. +fn mask_postgres_dsn(dsn: &str) -> std::string::String { let trimmed_dsn = dsn.trim(); if trimmed_dsn.is_empty() { return std::string::String::from(""); } - let queryless = crate::postgres::store::strip_query(trimmed_dsn); + let queryless = strip_query(trimmed_dsn); return match queryless.split_once("://") { std::option::Option::Some((scheme, remainder)) => { - crate::postgres::store::mask_scheme_remainder( - scheme, - remainder, - trimmed_dsn.contains('?'), - ) + mask_scheme_remainder(scheme, remainder, trimmed_dsn.contains('?')) }, - std::option::Option::None => crate::postgres::store::mask_plain_dsn(queryless.as_str()), + std::option::Option::None => mask_plain_dsn(queryless.as_str()), }; } @@ -422,7 +464,7 @@ fn strip_query(dsn: &str) -> std::string::String { } fn mask_scheme_remainder(scheme: &str, remainder: &str, had_query: bool) -> std::string::String { - let suffix = crate::postgres::store::query_suffix(had_query); + let suffix = query_suffix(had_query); return match remainder.rsplit_once('@') { std::option::Option::Some((_userinfo, host_path)) => { format!("{scheme}://***:***@{host_path}{suffix}") @@ -448,21 +490,51 @@ fn query_suffix(had_query: bool) -> std::string::String { #[cfg(test)] mod tests { #[test] - fn options_reject_empty_database_url() { + fn backend_options_reject_empty_database_url() { let result = crate::PostgresStoreOptions::new(" ", 1, 1000, false); assert!(result.is_err()); } #[test] - fn options_reject_zero_max_connections() { + fn backend_options_reject_zero_max_connections() { let result = crate::PostgresStoreOptions::new("postgres://localhost/db", 0, 1000, false); assert!(result.is_err()); } #[test] - fn options_reject_zero_connect_timeout() { - let result = crate::PostgresStoreOptions::new("postgres://localhost/db", 1, 0, false); - assert!(result.is_err()); + fn incomplete_backend_options_do_not_echo_secret_values() { + let options = serde_json::json!({ + "url": "postgres://operator:STORE-SECRET-CANARY@localhost/db", + "connect_timeout_ms": 5000, + "auto_initialize_schema": true + }); + let error = match crate::PostgresStoreOptions::from_backend_options(&options) { + std::result::Result::Ok(_) => panic!("incomplete backend options must be rejected"), + std::result::Result::Err(error) => error, + }; + assert!(!error.to_string().contains("STORE-SECRET-CANARY")); + assert!(!error.to_string().contains("postgres://")); + } + + #[test] + fn opaque_backend_options_are_sanitized_in_summary() { + let options = serde_json::json!({ + "url": "postgres://operator:STORE-SECRET-CANARY@localhost/db", + "max_connections": 4, + "connect_timeout_ms": 5000, + "auto_initialize_schema": true + }); + let parsed = crate::PostgresStoreOptions::from_backend_options(&options); + let value = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("backend options must parse: {error}"), + }; + let serialized = match serde_json::to_string(&value.configuration_summary()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("summary must serialize: {error}"), + }; + assert!(!serialized.contains("STORE-SECRET-CANARY")); + assert!(!serialized.contains("postgres://")); } #[tokio::test] @@ -471,7 +543,7 @@ mod tests { std::result::Result::Ok(value) => value, std::result::Result::Err(_error) => return, }; - let _postgres_guard = crate::postgres::test_serial::postgres_test_guard().await; + let _postgres_guard = crate::postgres_test_guard().await; let options_result = crate::PostgresStoreOptions::new(database_url, 1, 5000, false); let options = match options_result { std::result::Result::Ok(value) => value, @@ -488,25 +560,24 @@ mod tests { std::result::Result::Err(error) => panic!("unexpected health error: {error}"), }; assert_eq!(health.status, crate::StoreHealthStatus::Healthy); - return; } #[test] fn mask_postgres_dsn_masks_userinfo() { - let masked = crate::mask_postgres_dsn("postgres://user:secret@localhost:5432/db"); + let masked = super::mask_postgres_dsn("postgres://user:secret@localhost:5432/db"); assert_eq!(masked, "postgres://***:***@localhost:5432/db"); } #[test] fn mask_postgres_dsn_masks_query_string() { let masked = - crate::mask_postgres_dsn("postgres://localhost/db?sslmode=require&password=secret"); + super::mask_postgres_dsn("postgres://localhost/db?sslmode=require&password=secret"); assert_eq!(masked, "postgres://localhost/db?"); } #[test] fn mask_postgres_dsn_masks_plain_password_dsn() { - let masked = crate::mask_postgres_dsn("host=localhost password=secret dbname=khadhroony"); + let masked = super::mask_postgres_dsn("host=localhost password=secret dbname=khadhroony"); assert_eq!(masked, ""); } } diff --git a/ks-store/src/postgres/test_serial.rs b/ks-store/src/postgres/test_serial.rs index fd125bd..11fc8ca 100644 --- a/ks-store/src/postgres/test_serial.rs +++ b/ks-store/src/postgres/test_serial.rs @@ -1,5 +1,5 @@ // file: ks-store/src/postgres/test_serial.rs -// version: 2 +// version: 3 //! Test-only serialization helpers for optional real PostgreSQL tests. @@ -7,7 +7,7 @@ static POSTGRES_TEST_MUTEX: std::sync::OnceLock tokio::sync::OwnedMutexGuard<()> { +pub(crate) async fn postgres_test_guard() -> tokio::sync::OwnedMutexGuard<()> { let mutex = POSTGRES_TEST_MUTEX .get_or_init(|| return std::sync::Arc::new(tokio::sync::Mutex::new(()))) .clone(); diff --git a/ks-store/src/store.rs b/ks-store/src/store.rs new file mode 100644 index 0000000..1502f27 --- /dev/null +++ b/ks-store/src/store.rs @@ -0,0 +1,1023 @@ +// file: ks-store/src/store.rs +// version: 9 + +//! Backend-agnostic store facade and connection ownership. + +/// Backend-agnostic options used to open one persistent store. +#[derive(Clone, Eq, PartialEq)] +pub struct StoreOpenOptions { + enabled: bool, + backend: std::string::String, + backend_options: serde_json::Value, +} + +impl crate::StoreOpenOptions { + /// Creates validated store-open options without interpreting backend-specific fields. + pub fn new( + enabled: bool, + backend: impl std::convert::Into, + backend_options: serde_json::Value, + ) -> ks_core::Result { + let backend_value = backend.into(); + if backend_value.trim().is_empty() { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_invalid", + "store backend code must not be empty", + )); + } + if !backend_options.is_object() { + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_options_invalid", + "store backend options must be a JSON object", + )); + } + return std::result::Result::Ok(Self { + enabled, + backend: backend_value, + backend_options, + }); + } + + /// Returns whether persistence is enabled. + pub fn enabled(&self) -> bool { + return self.enabled; + } + + /// Returns the selected backend code without exposing backend options. + pub fn backend(&self) -> &str { + return self.backend.as_str(); + } + + /// Returns a backend-neutral summary after validating the selected backend options. + pub fn configuration_summary(&self) -> ks_core::Result { + if !self.enabled { + return std::result::Result::Ok(crate::StoreConfigurationSummary { + enabled: false, + backend_code: self.backend.clone(), + connection_configured: false, + auto_initialize_schema: false, + backend_option_count: backend_option_count(&self.backend_options), + }); + } + if self.backend == "postgres" { + let options = + match crate::PostgresStoreOptions::from_backend_options(&self.backend_options) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(options.configuration_summary()); + } + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_unsupported", + format!("unsupported store backend '{}'", self.backend).as_str(), + )); + } +} + +#[derive(Clone)] +enum StoreBackend { + Disabled, + Postgres(crate::PostgresStore), +} + +/// Clonable backend-agnostic persistent store handle. +#[derive(Clone)] +pub struct Store { + backend: StoreBackend, + configuration: crate::StoreConfigurationSummary, + initialization: crate::StoreInitializationSummary, +} + +impl crate::Store { + /// Opens the selected backend, optionally initializes its schema, and captures safe diagnostics. + pub async fn open(options: crate::StoreOpenOptions) -> ks_core::Result { + tracing::debug!( + target: crate::TRACING_TARGET, + action = "open_store", + backend = %options.backend, + enabled = options.enabled, + "open backend-agnostic store", + ); + if !options.enabled { + let option_count = backend_option_count(&options.backend_options); + return std::result::Result::Ok(Self { + backend: StoreBackend::Disabled, + configuration: crate::StoreConfigurationSummary { + enabled: false, + backend_code: options.backend, + connection_configured: false, + auto_initialize_schema: false, + backend_option_count: option_count, + }, + initialization: disabled_initialization_summary(), + }); + } + if options.backend == "postgres" { + return open_postgres(options.backend_options).await; + } + return std::result::Result::Err(crate::storage_contract_error( + "store_backend_unsupported", + format!("unsupported store backend '{}'", options.backend).as_str(), + )); + } + + /// Returns the safe configuration summary captured while opening the store. + pub fn configuration_summary(&self) -> &crate::StoreConfigurationSummary { + return &self.configuration; + } + + /// Returns the initialization summary captured while opening the store. + pub fn initialization_summary(&self) -> &crate::StoreInitializationSummary { + return &self.initialization; + } + + /// Reads a complete runtime summary without exposing backend secrets or physical object names. + pub async fn runtime_summary(&self) -> ks_core::Result { + let backend = match &self.backend { + StoreBackend::Disabled => std::option::Option::None, + StoreBackend::Postgres(store) => { + let diagnostics = match store.backend_diagnostics().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_error) => { + return std::result::Result::Err(crate::storage_contract_error( + "store_runtime_diagnostics_failed", + "store runtime diagnostics failed", + )); + }, + }; + std::option::Option::Some(diagnostics) + }, + }; + return std::result::Result::Ok(crate::StoreRuntimeSummary { + configuration: self.configuration.clone(), + backend, + initialization: self.initialization.clone(), + }); + } + + /// Reads diagnostics for level-1 raw resources. + pub async fn raw_resource_diagnostics( + &self, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Ok(std::vec::Vec::new()), + StoreBackend::Postgres(store) => sanitize_backend_result( + store.raw_resource_diagnostics().await, + "store_resource_diagnostics_failed", + "raw store resource diagnostics failed", + ), + }; + } + + /// Reads diagnostics for Core and processing resources. + pub async fn core_resource_diagnostics( + &self, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Ok(std::vec::Vec::new()), + StoreBackend::Postgres(store) => sanitize_backend_result( + store.core_resource_diagnostics().await, + "store_resource_diagnostics_failed", + "Core store resource diagnostics failed", + ), + }; + } + + /// Reads diagnostics for decode and materialization resources. + pub async fn decode_resource_diagnostics( + &self, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Ok(std::vec::Vec::new()), + StoreBackend::Postgres(store) => sanitize_backend_result( + store.decode_resource_diagnostics().await, + "store_resource_diagnostics_failed", + "decode store resource diagnostics failed", + ), + }; + } + + /// Reads diagnostics for every logical store resource known by the active backend. + pub async fn known_resource_diagnostics( + &self, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Ok(std::vec::Vec::new()), + StoreBackend::Postgres(store) => sanitize_backend_result( + store.known_resource_diagnostics().await, + "store_resource_diagnostics_failed", + "store resource diagnostics failed", + ), + }; + } + + /// Lists bounded replay transaction candidates. + pub async fn replay_transaction_candidates( + &self, + filter: &crate::ReplayTransactionFilter, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => store.replay_transaction_candidates(filter).await, + }; + } + + /// Lists bounded replay program summaries. + pub async fn replay_program_summaries( + &self, + filter: &crate::ReplayProgramFilter, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => store.replay_program_summaries(filter).await, + }; + } + + /// Lists bounded replay entity summaries. + pub async fn replay_entity_summaries( + &self, + filter: &crate::ReplayEntityFilter, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => store.replay_entity_summaries(filter).await, + }; + } +} + +async fn open_postgres(backend_options: serde_json::Value) -> ks_core::Result { + let postgres_options = match crate::PostgresStoreOptions::from_backend_options(&backend_options) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let configuration = postgres_options.configuration_summary(); + let auto_initialize_schema = postgres_options.auto_initialize_schema(); + let store = match crate::PostgresStore::connect(postgres_options).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let before = match store.known_resource_diagnostics().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_error) => { + return std::result::Result::Err(crate::storage_contract_error( + "store_initialization_verification_failed", + "store resource verification failed before initialization", + )); + }, + }; + if auto_initialize_schema { + let initialize_result = store.initialize_store_schema().await; + if let std::result::Result::Err(_error) = initialize_result { + return std::result::Result::Err(crate::storage_contract_error( + "store_initialization_failed", + "store schema initialization failed", + )); + } + } + let after = match store.known_resource_diagnostics().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_error) => { + return std::result::Result::Err(crate::storage_contract_error( + "store_initialization_verification_failed", + "store resource verification failed after initialization", + )); + }, + }; + return std::result::Result::Ok(crate::Store { + backend: StoreBackend::Postgres(store), + configuration, + initialization: build_initialization_summary(&before, &after), + }); +} + +fn sanitize_backend_result( + result: ks_core::Result, + code: &str, + message: &str, +) -> ks_core::Result { + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(_error) => { + std::result::Result::Err(crate::storage_contract_error(code, message)) + }, + }; +} + +fn backend_option_count(value: &serde_json::Value) -> u32 { + let count = match value.as_object() { + std::option::Option::Some(options) => options.len(), + std::option::Option::None => 0, + }; + return match u32::try_from(count) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => u32::MAX, + }; +} + +fn disabled_store_error() -> ks_core::Error { + return crate::storage_contract_error("store_disabled", "persistent store is disabled"); +} + +fn disabled_initialization_summary() -> crate::StoreInitializationSummary { + return crate::StoreInitializationSummary { + status: crate::StoreInitializationStatus::Disabled, + schema_contract_version: crate::STORE_SCHEMA_CONTRACT_VERSION.to_string(), + models: std::vec::Vec::new(), + expected_resource_count: 0, + available_resource_count: 0, + created_resource_count: 0, + objects: std::vec::Vec::new(), + }; +} + +fn build_initialization_summary( + before: &[crate::StoreResourceDiagnostics], + after: &[crate::StoreResourceDiagnostics], +) -> crate::StoreInitializationSummary { + let model_codes = ["raw", "core", "processing", "decode", "materialization"]; + let mut models = std::vec::Vec::new(); + let mut expected_resource_count = 0_u32; + let mut available_resource_count = 0_u32; + let mut created_resource_count = 0_u32; + for model_code in model_codes { + let mut expected = 0_u32; + let mut available = 0_u32; + let mut created = 0_u32; + for resource in after { + if resource.model_code != model_code { + continue; + } + expected += 1; + if resource.available { + available += 1; + if resource_was_available(before, resource.resource_code.as_str()) + == std::option::Option::Some(false) + { + created += 1; + } + } + } + if expected == 0 { + continue; + } + let missing = expected.saturating_sub(available); + models.push(crate::StoreModelVerificationSummary { + model_code: model_code.to_string(), + status: if missing == 0 { + crate::StoreModelVerificationStatus::Ready + } else { + crate::StoreModelVerificationStatus::Incomplete + }, + expected_resource_count: expected, + available_resource_count: available, + created_resource_count: created, + missing_resource_count: missing, + }); + expected_resource_count += expected; + available_resource_count += available; + created_resource_count += created; + } + let status = if expected_resource_count == available_resource_count { + crate::StoreInitializationStatus::Ready + } else { + crate::StoreInitializationStatus::Partial + }; + return crate::StoreInitializationSummary { + status, + schema_contract_version: crate::STORE_SCHEMA_CONTRACT_VERSION.to_string(), + models, + expected_resource_count, + available_resource_count, + created_resource_count, + objects: std::vec![crate::StoreObjectVerificationSummary { + object_kind: "table".to_string(), + expected_count: expected_resource_count, + available_count: available_resource_count, + created_count: created_resource_count, + }], + }; +} + +fn resource_was_available( + before: &[crate::StoreResourceDiagnostics], + resource_code: &str, +) -> std::option::Option { + for resource in before { + if resource.resource_code == resource_code { + return std::option::Option::Some(resource.available); + } + } + return std::option::Option::None; +} + +#[async_trait::async_trait] +impl crate::StoreHealthStore for crate::Store { + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn backend_descriptor(&self) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => crate::StoreBackendDescriptor::new( + self.configuration.backend_code.clone(), + "Disabled", + std::option::Option::None, + std::option::Option::None, + ), + StoreBackend::Postgres(store) => store.backend_descriptor().await, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn health_snapshot(&self) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => crate::StoreHealthSnapshot::new( + self.configuration.backend_code.clone(), + crate::StoreHealthStatus::Unknown, + std::option::Option::Some("store disabled".to_string()), + ), + StoreBackend::Postgres(store) => store.health_snapshot().await, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn migration_snapshot(&self) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Ok(crate::StoreMigrationSnapshot::new( + crate::StoreMigrationStatus::NotInitialized, + std::option::Option::None, + std::vec::Vec::new(), + std::option::Option::Some("store disabled".to_string()), + )), + StoreBackend::Postgres(store) => store.migration_snapshot().await, + }; + } +} + +#[async_trait::async_trait] +impl crate::RawTransactionStore for crate::Store { + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn has_raw_transaction_signature( + &self, + signature: &ks_lib::MdSignature, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::RawTransactionStore::has_raw_transaction_signature(store, signature).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn has_transaction_observation_key( + &self, + observation_key: &str, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::RawTransactionStore::has_transaction_observation_key(store, observation_key) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_raw_transaction( + &self, + input: &crate::RawTransactionInsert, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::RawTransactionStore::insert_raw_transaction(store, input).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_transaction_observation( + &self, + input: &crate::TransactionObservationInsert, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::RawTransactionStore::insert_transaction_observation(store, input).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn mark_raw_payload_lifecycle( + &self, + mark: &crate::RawPayloadLifecycleMark, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::RawTransactionStore::mark_raw_payload_lifecycle(store, mark).await + }, + }; + } +} + +#[async_trait::async_trait] +impl crate::CoreTransactionStore for crate::Store { + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_core_transaction( + &self, + input: &crate::CoreTransactionInsert, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::insert_core_transaction(store, input).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_core_account_keys( + &self, + inputs: &[crate::CoreAccountKeyInsert], + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::insert_core_account_keys(store, inputs).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_core_instructions( + &self, + inputs: &[crate::CoreInstructionInsert], + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::insert_core_instructions(store, inputs).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_core_inner_instructions( + &self, + inputs: &[crate::CoreInnerInstructionInsert], + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::insert_core_inner_instructions(store, inputs).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_core_logs( + &self, + inputs: &[crate::CoreLogInsert], + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::insert_core_logs(store, inputs).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn insert_core_balance_changes( + &self, + inputs: &[crate::CoreBalanceChangeInsert], + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::insert_core_balance_changes(store, inputs).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn list_core_instructions_for_replay( + &self, + filter: &crate::CoreInstructionReplayFilter, + page_request: &crate::PageRequest, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::list_core_instructions_for_replay( + store, + filter, + page_request, + ) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn list_core_instruction_replay_inputs( + &self, + filter: &crate::CoreInstructionReplayFilter, + page_request: &crate::PageRequest, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::list_core_instruction_replay_inputs( + store, + filter, + page_request, + ) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn mark_core_instruction_lifecycle( + &self, + mark: &crate::CoreInstructionLifecycleMark, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreTransactionStore::mark_core_instruction_lifecycle(store, mark).await + }, + }; + } +} + +#[async_trait::async_trait] +impl crate::CoreExtractionStore for crate::Store { + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn list_raw_transactions_for_core_extraction( + &self, + filter: &crate::CoreExtractionSelectionFilter, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreExtractionStore::list_raw_transactions_for_core_extraction(store, filter) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn is_core_extraction_current( + &self, + identity: &crate::ProcessingLedgerIdentity, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreExtractionStore::is_core_extraction_current(store, identity).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn persist_core_extraction( + &self, + bundle: &crate::CoreExtractionBundle, + force_replay: bool, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreExtractionStore::persist_core_extraction(store, bundle, force_replay) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn mark_core_extraction_failed( + &self, + failure: &crate::CoreExtractionFailure, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::CoreExtractionStore::mark_core_extraction_failed(store, failure).await + }, + }; + } +} + +#[async_trait::async_trait] +impl crate::DecodePipelineStore for crate::Store { + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn list_decode_inputs( + &self, + filter: &crate::DecodeSelectionFilter, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::list_decode_inputs(store, filter).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn is_decode_current( + &self, + identity: &crate::ProcessingLedgerIdentity, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::is_decode_current(store, identity).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn persist_decode_coverage_declarations( + &self, + declarations: &[crate::DecodeCoverageDeclarationInsert], + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::persist_decode_coverage_declarations( + store, + declarations, + ) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn persist_decode_result( + &self, + bundle: &crate::DecodePersistenceBundle, + force_replay: bool, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::persist_decode_result(store, bundle, force_replay).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn mark_decode_failed( + &self, + failure: &crate::DecodeFailure, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::mark_decode_failed(store, failure).await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn persist_materialization_result( + &self, + bundle: &crate::MaterializationPersistenceBundle, + force_replay: bool, + ) -> ks_core::Result { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::persist_materialization_result( + store, + bundle, + force_replay, + ) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn list_decode_coverage_summary( + &self, + processor_name: std::option::Option<&str>, + processor_version: std::option::Option<&str>, + limit: u32, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::list_decode_coverage_summary( + store, + processor_name, + processor_version, + limit, + ) + .await + }, + }; + } + + #[expect( + clippy::implicit_return, + reason = "async_trait expansion triggers implicit_return on generated async trait methods." + )] + async fn list_materialized_events( + &self, + filter: &crate::MaterializedEventFilter, + ) -> ks_core::Result> { + return match &self.backend { + StoreBackend::Disabled => std::result::Result::Err(disabled_store_error()), + StoreBackend::Postgres(store) => { + crate::DecodePipelineStore::list_materialized_events(store, filter).await + }, + }; + } +} + +#[cfg(test)] +mod tests { + #[test] + fn backend_diagnostic_errors_are_sanitized_at_the_facade_boundary() { + let result: ks_core::Result<()> = std::result::Result::Err(ks_core::Error::db( + "backend detail references kb_sol_private_table", + )); + let error = match super::sanitize_backend_result( + result, + "store_resource_diagnostics_failed", + "store resource diagnostics failed", + ) { + std::result::Result::Ok(()) => panic!("backend error must remain an error"), + std::result::Result::Err(error) => error, + }; + assert!(!error.to_string().contains("kb_sol_private_table")); + assert!(error.to_string().contains("store resource diagnostics failed")); + } + + #[test] + fn initialization_summary_reports_backend_objects_without_physical_names() { + let before = std::vec![crate::StoreResourceDiagnostics { + resource_code: "raw_transactions".to_string(), + model_code: "raw".to_string(), + role: "canonical raw transaction".to_string(), + available: false, + statistics: std::option::Option::None, + }]; + let after = std::vec![crate::StoreResourceDiagnostics { + resource_code: "raw_transactions".to_string(), + model_code: "raw".to_string(), + role: "canonical raw transaction".to_string(), + available: true, + statistics: std::option::Option::None, + }]; + let summary = super::build_initialization_summary(&before, &after); + assert_eq!(summary.objects.len(), 1); + assert_eq!(summary.objects[0].object_kind, "table"); + assert_eq!(summary.objects[0].expected_count, 1); + assert_eq!(summary.objects[0].available_count, 1); + assert_eq!(summary.objects[0].created_count, 1); + let serialized = match serde_json::to_string(&summary) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("summary must serialize: {error}"), + }; + assert!(!serialized.contains("kb_sol_")); + } + + #[test] + fn store_open_options_reject_non_object_backend_options() { + let result = crate::StoreOpenOptions::new(true, "postgres", serde_json::Value::Null); + assert!(result.is_err()); + } + + #[tokio::test] + async fn unknown_backend_is_rejected_without_echoing_options() { + let options = match crate::StoreOpenOptions::new( + true, + "unknown", + serde_json::json!({"password": "STORE-SECRET-CANARY"}), + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("open options must parse: {error}"), + }; + let error = match crate::Store::open(options).await { + std::result::Result::Ok(_) => panic!("unknown backend must be rejected"), + std::result::Result::Err(error) => error, + }; + assert!(!error.to_string().contains("STORE-SECRET-CANARY")); + } + + #[tokio::test] + async fn disabled_store_opens_without_interpreting_backend_options() { + let options = match crate::StoreOpenOptions::new( + false, + "future-backend", + serde_json::json!({"secret": "STORE-SECRET-CANARY"}), + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("open options must parse: {error}"), + }; + let store = match crate::Store::open(options).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("disabled store must open: {error}"), + }; + let summary = store.configuration_summary(); + let serialized = match serde_json::to_string(summary) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("summary must serialize: {error}"), + }; + assert!(!serialized.contains("STORE-SECRET-CANARY")); + assert_eq!( + store.initialization_summary().status, + crate::StoreInitializationStatus::Disabled + ); + } +} diff --git a/scripts/audit_khadhroony_workspace_rules.py b/scripts/audit_khadhroony_workspace_rules.py index de7acd2..fa56027 100644 --- a/scripts/audit_khadhroony_workspace_rules.py +++ b/scripts/audit_khadhroony_workspace_rules.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_khadhroony_workspace_rules.py -# version: 28 +# version: 31 """Audit mechanically verifiable rules specific to khadhroony-bot3.""" @@ -1097,6 +1097,38 @@ def audit_configuration_split(root: pathlib.Path) -> list[Violation]: violations.append(Violation("KH_CFG013", relative, index, "resolved/source configuration contracts must not derive Serialize because they may contain resolved secrets")) if re.search(r"\bDebug\b", line): violations.append(Violation("KH_CFG014", relative, index, "resolved/source configuration contracts must not derive Debug because they may contain resolved secrets or internal values")) + config_lib = root / "ks-config/src/lib.rs" + if config_lib.is_file(): + config_exported_types = { + match.group(1) + for match in re.finditer( + r"(?m)^pub(?:\(crate\))? use self::[^;]+::([A-Z][A-Za-z0-9_]*)\s*;", + config_lib.read_text(encoding="utf-8"), + ) + } + for path in sorted((root / "ks-config/src").glob("*.rs")): + if path.name == "lib.rs": + continue + relative = path.relative_to(root).as_posix() + for index, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = line.lstrip() + if stripped.startswith("//") or stripped.startswith("#["): + continue + code = re.sub(r'"(?:\\.|[^"\\])*"', '""', line) + for type_name in sorted(config_exported_types): + declaration_pattern = rf"\bpub(?:\([^)]*\))?\s+(?:struct|enum|type|trait)\s+{re.escape(type_name)}\b" + if re.search(declaration_pattern, code): + continue + if re.search(rf"(? list[Violation]: ) return violations + +def audit_store_backend_boundary(root: pathlib.Path) -> list[Violation]: + """Keep concrete storage backends private to `ks-store`.""" + + violations: list[Violation] = [] + forbidden_external = { + "sqlx::": "SQLx must not cross the public ks-store boundary", + "PgPool": "PostgreSQL pool types must remain private to ks-store", + "PostgresStore": "PostgresStore must remain private to ks-store", + "PostgresStoreOptions": "PostgresStoreOptions must remain private to ks-store", + "PostgresReplay": "Postgres-prefixed replay contracts must not escape ks-store", + "ks_store::postgres": "backend module paths must not escape ks-store", + } + for path in sorted(root.glob("*/src/**/*.rs")): + relative_path = path.relative_to(root) + if relative_path.parts[0] == "ks-store": + continue + text = path.read_text(encoding="utf-8") + relative = relative_path.as_posix() + for fragment, message in forbidden_external.items(): + start = 0 + while True: + offset = text.find(fragment, start) + if offset < 0: + break + violations.append( + Violation( + "KH_STORE001", + relative, + text[:offset].count("\n") + 1, + message, + ) + ) + start = offset + len(fragment) + crate_root = root / "ks-store/src/lib.rs" + if crate_root.is_file(): + text = crate_root.read_text(encoding="utf-8") + for index, line in enumerate(text.splitlines(), 1): + if line.startswith("pub use ") and any(fragment in line for fragment in ("Postgres", "PgPool", "sqlx")): + violations.append( + Violation( + "KH_STORE002", + "ks-store/src/lib.rs", + index, + "public ks-store exports must remain backend-agnostic", + ) + ) + postgres_root = root / "ks-store/src/postgres" + if postgres_root.is_dir(): + for path in sorted(postgres_root.rglob("*.rs")): + text = path.read_text(encoding="utf-8") + relative = path.relative_to(root).as_posix() + for match in re.finditer(r"tracing::(?:trace|debug|info|warn|error)!\(", text): + end = text.find(");", match.start()) + fragment = text[match.start():] if end < 0 else text[match.start(): end + 2] + line = text[:match.start()].count("\n") + 1 + if "target: crate::TRACING_TARGET" not in fragment: + violations.append( + Violation( + "KH_STORE003", + relative, + line, + "PostgreSQL tracing must use the canonical `crate::TRACING_TARGET`", + ) + ) + continue + if 'backend = "postgres"' not in fragment or 'domain = "ks-store.pg"' not in fragment: + violations.append( + Violation( + "KH_STORE004", + relative, + line, + "PostgreSQL tracing must declare `backend = \"postgres\"` and `domain = \"ks-store.pg\"`", + ) + ) + if "action =" not in fragment: + violations.append( + Violation( + "KH_STORE006", + relative, + line, + "PostgreSQL tracing must declare one structured `action` field", + ) + ) + cargo = root / "ks-store/Cargo.toml" + if cargo.is_file(): + data = tomllib.loads(cargo.read_text(encoding="utf-8")) + if "ks-config" in data.get("dependencies", {}): + violations.append( + Violation( + "KH_STORE005", + "ks-store/Cargo.toml", + 1, + "ks-store must not depend on ks-config", + ) + ) + return violations + + def audit_wincode_resolution(root: pathlib.Path) -> list[Violation]: """Require the currently compatible Solana wincode dependency family.""" @@ -1395,6 +1526,7 @@ def main() -> int: +audit_ts_rs_boundaries(root) +audit_configuration_split(root) +audit_sensitive_transport_surfaces(root) + +audit_store_backend_boundary(root) +audit_wincode_resolution(root) ) violations.sort(key=lambda item: (item.code, item.path, item.line, item.message)) diff --git a/scripts/audit_rust_general_rules.py b/scripts/audit_rust_general_rules.py index 1031024..2410001 100755 --- a/scripts/audit_rust_general_rules.py +++ b/scripts/audit_rust_general_rules.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_rust_general_rules.py -# version: 10 +# version: 11 """Audit mechanically verifiable Rust rules shared by all Rust projects.""" @@ -77,6 +77,15 @@ def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]: violations.append(Violation("RUST003", relative, max(len(lines), 1), "file must end with exactly one newline")) for index, line in enumerate(lines, 1): stripped = line.strip() + if "pub(in " in stripped or "pub(super)" in stripped: + violations.append( + Violation( + "RUST027", + relative, + index, + "restricted public visibility is forbidden; use private or `pub(crate)` with crate-root re-export", + ) + ) if stripped.startswith("pub mod "): violations.append( Violation( diff --git a/test-fixtures/config/example.resolved.app.config.json b/test-fixtures/config/example.resolved.app.config.json index a0118bc..11c9724 100644 --- a/test-fixtures/config/example.resolved.app.config.json +++ b/test-fixtures/config/example.resolved.app.config.json @@ -6,19 +6,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_DEVNET_URL}", "max_connections": 8, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/local.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } }, "solana": { diff --git a/test-fixtures/config/resolved.app.config.json b/test-fixtures/config/resolved.app.config.json index 563b6bd..ee328d1 100644 --- a/test-fixtures/config/resolved.app.config.json +++ b/test-fixtures/config/resolved.app.config.json @@ -6,19 +6,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_DEVNET_URL}", "max_connections": 8, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/local.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } }, "solana": { @@ -229,19 +221,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_MAINNET_URL}", "max_connections": 16, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/mainnet-import.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } }, "solana": { @@ -652,19 +636,11 @@ "database": { "enabled": true, "backend": "postgres", - "postgres": { + "backend_options": { "url": "${KS_SECRET_POSTGRES_MAINNET_URL}", "max_connections": 16, "connect_timeout_ms": 5000, "auto_initialize_schema": true - }, - "sqlite": { - "path": "data/mainnet.sqlite", - "create_if_missing": true, - "busy_timeout_ms": 5000, - "max_connections": 1, - "auto_initialize_schema": true, - "use_wal": true } }, "solana": {