// file: kb_lib/src/db/queries/chain_slot.rs //! Queries for `kb_chain_slots`. /// Inserts or updates one normalized chain slot row. pub async fn upsert_chain_slot( database: &crate::KbDatabase, dto: &crate::KbChainSlotDto, ) -> Result { let slot_result = i64::try_from(dto.slot); let slot = match slot_result { Ok(slot) => slot, Err(error) => { return Err(crate::KbError::Db(format!( "cannot convert chain slot '{}' to i64: {}", dto.slot, error ))); } }; let parent_slot = match dto.parent_slot { Some(parent_slot) => { let parent_slot_result = i64::try_from(parent_slot); match parent_slot_result { Ok(parent_slot) => Some(parent_slot), Err(error) => { return Err(crate::KbError::Db(format!( "cannot convert chain parent_slot '{}' to i64: {}", parent_slot, error ))); } } } None => None, }; match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query( r#" INSERT INTO kb_chain_slots ( slot, parent_slot, block_time_unix, created_at, updated_at ) VALUES (?, ?, ?, ?, ?) ON CONFLICT(slot) DO UPDATE SET parent_slot = excluded.parent_slot, block_time_unix = excluded.block_time_unix, updated_at = excluded.updated_at "#, ) .bind(slot) .bind(parent_slot) .bind(dto.block_time_unix) .bind(dto.created_at.to_rfc3339()) .bind(dto.updated_at.to_rfc3339()) .execute(pool) .await; if let Err(error) = query_result { return Err(crate::KbError::Db(format!( "cannot upsert kb_chain_slots on sqlite: {}", error ))); } Ok(dto.slot) } } } /// Reads one chain slot row by slot number. pub async fn get_chain_slot( database: &crate::KbDatabase, slot: u64, ) -> Result, crate::KbError> { let slot_result = i64::try_from(slot); let slot_i64 = match slot_result { Ok(slot_i64) => slot_i64, Err(error) => { return Err(crate::KbError::Db(format!( "cannot convert requested chain slot '{}' to i64: {}", slot, error ))); } }; match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query_as::( r#" SELECT slot, parent_slot, block_time_unix, created_at, updated_at FROM kb_chain_slots WHERE slot = ? LIMIT 1 "#, ) .bind(slot_i64) .fetch_optional(pool) .await; let entity_option = match query_result { Ok(entity_option) => entity_option, Err(error) => { return Err(crate::KbError::Db(format!( "cannot fetch kb_chain_slots for slot '{}' on sqlite: {}", slot, error ))); } }; match entity_option { Some(entity) => { let dto_result = crate::KbChainSlotDto::try_from(entity); match dto_result { Ok(dto) => Ok(Some(dto)), Err(error) => Err(error), } } None => Ok(None), } } } } /// Lists recent chain slots ordered from newest to oldest. pub async fn list_recent_chain_slots( database: &crate::KbDatabase, limit: u32, ) -> Result, crate::KbError> { if limit == 0 { return Ok(std::vec::Vec::new()); } match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query_as::( r#" SELECT slot, parent_slot, block_time_unix, created_at, updated_at FROM kb_chain_slots ORDER BY slot DESC LIMIT ? "#, ) .bind(i64::from(limit)) .fetch_all(pool) .await; let entities = match query_result { Ok(entities) => entities, Err(error) => { return Err(crate::KbError::Db(format!( "cannot list kb_chain_slots on sqlite: {}", error ))); } }; let mut dtos = std::vec::Vec::new(); for entity in entities { let dto_result = crate::KbChainSlotDto::try_from(entity); let dto = match dto_result { Ok(dto) => dto, Err(error) => return Err(error), }; dtos.push(dto); } Ok(dtos) } } } #[cfg(test)] mod tests { async fn make_database() -> crate::KbDatabase { let tempdir = tempfile::tempdir().expect("tempdir must succeed"); let database_path = tempdir.path().join("chain_slot.sqlite3"); let config = crate::KbDatabaseConfig { enabled: true, backend: crate::KbDatabaseBackend::Sqlite, sqlite: crate::KbSqliteDatabaseConfig { 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, }, }; crate::KbDatabase::connect_and_initialize(&config) .await .expect("database init must succeed") } #[tokio::test] async fn chain_slot_roundtrip_works() { let database = make_database().await; let dto = crate::KbChainSlotDto::new(424242, Some(424241), Some(1_777_777_777)); let slot = crate::upsert_chain_slot(&database, &dto) .await .expect("chain slot upsert must succeed"); assert_eq!(slot, 424242); let fetched = crate::get_chain_slot(&database, 424242) .await .expect("chain slot fetch must succeed") .expect("chain slot must exist"); assert_eq!(fetched.slot, 424242); assert_eq!(fetched.parent_slot, Some(424241)); assert_eq!(fetched.block_time_unix, Some(1_777_777_777)); let listed = crate::list_recent_chain_slots(&database, 10) .await .expect("chain slot list must succeed"); assert_eq!(listed.len(), 1); assert_eq!(listed[0].slot, 424242); assert_eq!(listed[0].parent_slot, Some(424241)); assert_eq!(listed[0].block_time_unix, Some(1_777_777_777)); } }