This commit is contained in:
2026-05-27 18:45:16 +02:00
parent d9558a5c16
commit 96b6209482
18 changed files with 996 additions and 23 deletions

View File

@@ -0,0 +1,285 @@
// file: kb_lib/src/db/queries/dex_decode_replay_ledger.rs
//! Queries for `k_sol_dex_decode_replay_ledger`.
/// Inserts or updates one DEX decode replay ledger row.
pub async fn query_dex_decode_replay_ledger_upsert(
database: &crate::Database,
dto: &crate::DexDecodeReplayLedgerDto,
) -> Result<i64, crate::Error> {
let force_replay_required = if dto.force_replay_required { 1_i64 } else { 0_i64 };
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query(
r#"
INSERT INTO k_sol_dex_decode_replay_ledger (
transaction_id,
signature,
decoder_scope,
decoder_version,
decode_status,
certainty,
event_count,
distinct_token_mint_count,
force_replay_required,
status_reason,
created_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(transaction_id, decoder_scope, decoder_version) DO UPDATE SET
signature = excluded.signature,
decode_status = excluded.decode_status,
certainty = excluded.certainty,
event_count = excluded.event_count,
distinct_token_mint_count = excluded.distinct_token_mint_count,
force_replay_required = excluded.force_replay_required,
status_reason = excluded.status_reason,
updated_at = excluded.updated_at
"#,
)
.bind(dto.transaction_id)
.bind(dto.signature.clone())
.bind(dto.decoder_scope.clone())
.bind(dto.decoder_version.clone())
.bind(dto.decode_status.clone())
.bind(dto.certainty.clone())
.bind(dto.event_count)
.bind(dto.distinct_token_mint_count)
.bind(force_replay_required)
.bind(dto.status_reason.clone())
.bind(dto.created_at.to_rfc3339())
.bind(dto.updated_at.to_rfc3339())
.execute(pool)
.await;
if let Err(error) = query_result {
return Err(crate::Error::Db(format!(
"cannot upsert k_sol_dex_decode_replay_ledger on sqlite: {}",
error
)));
}
let id_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM k_sol_dex_decode_replay_ledger
WHERE transaction_id = ?
AND decoder_scope = ?
AND decoder_version = ?
LIMIT 1
"#,
)
.bind(dto.transaction_id)
.bind(dto.decoder_scope.clone())
.bind(dto.decoder_version.clone())
.fetch_one(pool)
.await;
match id_result {
Ok(id) => return Ok(id),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch k_sol_dex_decode_replay_ledger id for transaction_id '{}' on sqlite: {}",
dto.transaction_id, error
)));
},
}
},
}
}
/// Reads one replay ledger row by transaction id, decoder scope, and decoder version.
pub async fn query_dex_decode_replay_ledger_get_by_transaction(
database: &crate::Database,
transaction_id: i64,
decoder_scope: &str,
decoder_version: &str,
) -> Result<std::option::Option<crate::DexDecodeReplayLedgerDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result =
sqlx::query_as::<sqlx::Sqlite, crate::DexDecodeReplayLedgerEntity>(
r#"
SELECT
id,
transaction_id,
signature,
decoder_scope,
decoder_version,
decode_status,
certainty,
event_count,
distinct_token_mint_count,
force_replay_required,
status_reason,
created_at,
updated_at
FROM k_sol_dex_decode_replay_ledger
WHERE transaction_id = ?
AND decoder_scope = ?
AND decoder_version = ?
LIMIT 1
"#,
)
.bind(transaction_id)
.bind(decoder_scope.to_string())
.bind(decoder_version.to_string())
.fetch_optional(pool)
.await;
let entity_option = match query_result {
Ok(entity_option) => entity_option,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch k_sol_dex_decode_replay_ledger for transaction_id '{}' on sqlite: {}",
transaction_id, error
)));
},
};
match entity_option {
Some(entity) => {
let dto_result = crate::DexDecodeReplayLedgerDto::try_from(entity);
match dto_result {
Ok(dto) => return Ok(Some(dto)),
Err(error) => return Err(error),
}
},
None => return Ok(None),
}
},
}
}
/// Reads one replay ledger row by signature, decoder scope, and decoder version.
pub async fn query_dex_decode_replay_ledger_get_by_signature(
database: &crate::Database,
signature: &str,
decoder_scope: &str,
decoder_version: &str,
) -> Result<std::option::Option<crate::DexDecodeReplayLedgerDto>, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result =
sqlx::query_as::<sqlx::Sqlite, crate::DexDecodeReplayLedgerEntity>(
r#"
SELECT
id,
transaction_id,
signature,
decoder_scope,
decoder_version,
decode_status,
certainty,
event_count,
distinct_token_mint_count,
force_replay_required,
status_reason,
created_at,
updated_at
FROM k_sol_dex_decode_replay_ledger
WHERE signature = ?
AND decoder_scope = ?
AND decoder_version = ?
LIMIT 1
"#,
)
.bind(signature.to_string())
.bind(decoder_scope.to_string())
.bind(decoder_version.to_string())
.fetch_optional(pool)
.await;
let entity_option = match query_result {
Ok(entity_option) => entity_option,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch k_sol_dex_decode_replay_ledger for signature '{}' on sqlite: {}",
signature, error
)));
},
};
match entity_option {
Some(entity) => {
let dto_result = crate::DexDecodeReplayLedgerDto::try_from(entity);
match dto_result {
Ok(dto) => return Ok(Some(dto)),
Err(error) => return Err(error),
}
},
None => return Ok(None),
}
},
}
}
#[cfg(test)]
mod tests {
async fn make_database() -> crate::Database {
let tempdir = tempfile::tempdir().expect("tempdir must succeed");
let database_path = tempdir.path().join("dex_decode_replay_ledger.sqlite3");
let config = crate::DatabaseConfig {
enabled: true,
backend: crate::DatabaseBackend::Sqlite,
sqlite: crate::SqliteDatabaseConfig {
path: database_path.to_string_lossy().to_string(),
create_if_missing: true,
busy_timeout_ms: 5000,
max_connections: 1,
auto_initialize_schema: true,
use_wal: true,
},
};
return crate::Database::connect_and_initialize(&config)
.await
.expect("database init must succeed");
}
async fn insert_chain_transaction(database: &crate::Database) -> i64 {
let slot_dto = crate::ChainSlotDto::new(777, Some(776), Some(1_700_000_007));
crate::query_chain_slots_upsert(database, &slot_dto)
.await
.expect("slot upsert must succeed");
let dto = crate::ChainTransactionDto::new(
"ledger_sig".to_string(),
Some(777),
Some(1_700_000_007),
Some("test".to_string()),
None,
None,
Some("{}".to_string()),
"{}".to_string(),
);
return crate::query_chain_transactions_upsert(database, &dto)
.await
.expect("transaction upsert must succeed");
}
#[tokio::test]
async fn dex_decode_replay_ledger_roundtrip_works() {
let database = make_database().await;
let transaction_id = insert_chain_transaction(&database).await;
let dto = crate::DexDecodeReplayLedgerDto::new(
transaction_id,
"ledger_sig".to_string(),
"dex_decode.local_pipeline".to_string(),
"test-version".to_string(),
crate::DexDecodeReplayLedgerDto::STATUS_DECODED.to_string(),
crate::DexDecodeReplayLedgerDto::CERTAINTY_SURE.to_string(),
1,
2,
false,
Some("single-pair decode completed".to_string()),
);
let upsert_id = crate::query_dex_decode_replay_ledger_upsert(&database, &dto)
.await
.expect("ledger upsert must succeed");
assert!(upsert_id > 0);
let fetched = crate::query_dex_decode_replay_ledger_get_by_signature(
&database,
"ledger_sig",
"dex_decode.local_pipeline",
"test-version",
)
.await
.expect("ledger fetch must succeed")
.expect("ledger row must exist");
assert_eq!(fetched.transaction_id, transaction_id);
assert!(fetched.can_skip_decode());
}
}