From 45830a7985dec1dff06a4564975ffbcf2e01903c Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Sun, 30 Aug 2026 20:32:16 +0300 Subject: [PATCH] Write SqlDbLayer --- src/data.rs | 4 +- src/data/entities.rs | 8 +-- src/data/repositories.rs | 8 +-- src/data_provider/card_states.rs | 22 +++--- src/data_provider/decks.rs | 1 + src/data_provider/mod.rs | 120 +++++++++++++++++++++++++++++++ src/data_provider/sqlite.rs | 6 ++ 7 files changed, 150 insertions(+), 19 deletions(-) diff --git a/src/data.rs b/src/data.rs index 6c0c113..e575bf5 100644 --- a/src/data.rs +++ b/src/data.rs @@ -3,7 +3,7 @@ pub mod repositories; use rusqlite::ToSql; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; -use std::fmt::{Display, Formatter}; +use std::fmt::{write, Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; @@ -60,4 +60,4 @@ impl Id { pub(crate) fn is_valid(self) -> bool { self.0 != 0 } -} +} \ No newline at end of file diff --git a/src/data/entities.rs b/src/data/entities.rs index 5b1b6a0..557866b 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -185,7 +185,7 @@ impl CardStatistics { #[derive(Copy)] pub struct EntityHandle { pub(crate) id: Id, - index: i32, + index: u32, pub(crate) _phantom: PhantomData, } @@ -202,7 +202,7 @@ impl EntityHandle { pub fn new(id: Id, index: usize, real: bool) -> Self { Self { id, - index: index as i32 | (!real as i32) << 31, + index: ((index as u32) << 1 >> 1) | (!real as u32) << 31, _phantom: Default::default(), } } @@ -211,7 +211,7 @@ impl EntityHandle { } pub fn set_index(&mut self, index: usize) { - self.index = self.index >> 31 << 31 | index as i32; + self.index = (self.index >> 31 << 31) | ((index as u32) << 1 >> 1); } pub fn real(&self) -> bool { @@ -219,7 +219,7 @@ impl EntityHandle { } pub fn set_real(&mut self, value: bool) { - self.index = self.index << 1 >> 1 | (!value as i32) << 31; + self.index = self.index << 1 >> 1 | (!value as u32) << 31; } } impl Eq for EntityHandle {} diff --git a/src/data/repositories.rs b/src/data/repositories.rs index 93356f0..b382460 100644 --- a/src/data/repositories.rs +++ b/src/data/repositories.rs @@ -178,8 +178,8 @@ pub trait BatchedDbLayer where T: EntityData, { - fn delete_many_entities(&mut self, ids: &[Id], connection: &Connection) -> MaybeError; - fn add_many_entities(&mut self, entity: &mut [T], connection: &Connection) -> MaybeError; + fn delete_many_entities(&mut self, ids: &[Id], connection: &mut Connection) -> MaybeError; + fn add_many_entities(&mut self, entity: &mut [T], connection: &mut Connection) -> MaybeError; } #[derive(Clone)] @@ -414,7 +414,7 @@ impl + DbLayer> BatchedRepository }); self.data_layer - .add_many_entities(&mut temp_values, &self.connection.read())?; + .add_many_entities(&mut temp_values, &mut self.connection.write())?; let start = self.cache.len(); @@ -456,7 +456,7 @@ impl + DbLayer> BatchedRepository !remove }); self.data_layer - .delete_many_entities(cached.as_slice(), &self.connection.read())?; + .delete_many_entities(cached.as_slice(), &mut self.connection.write())?; let len = cached.len(); cell = 0; self.cache.retain(|x| { diff --git a/src/data_provider/card_states.rs b/src/data_provider/card_states.rs index 248e156..0af64da 100644 --- a/src/data_provider/card_states.rs +++ b/src/data_provider/card_states.rs @@ -23,7 +23,7 @@ pub fn load_stats_of_deck( score: row.get(2)?, fsrs_opened: NaiveDate::from_epoch_days(row.get::(4)?) .expect("Incorrect date encoding"), - fsrs_memory: Some(get_memory(row.get(5)?)), + fsrs_memory: get_memory(row.get(5)?), last_open: row.get(3)?, fsrs_open_day: NaiveDate::from_epoch_days(row.get::(6)?) .expect("Incorrect date encoding"), @@ -37,14 +37,21 @@ pub fn load_stats_of_deck( Ok(buffer) } -fn get_memory(value: i64) -> MemoryState { - MemoryState { +fn get_memory(value: i64) -> Option { + if value == 0 { + return None; + } + Some(MemoryState { stability: f32::from_bits(value as u32), difficulty: f32::from_bits((value >> 32) as u32), - } + }) } -fn memory_to_i64(value: &MemoryState) -> i64 { +fn memory_to_i64(value: &Option) -> i64 { + let Some(value) = &value else { + return 0; + }; + value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32) } @@ -69,10 +76,7 @@ pub fn add_stat_list(stats: &mut [CardStatistics], connection: &mut Connection) stat.score, stat.last_open.timestamp(), stat.fsrs_opened.to_epoch_days(), - memory_to_i64(&stat.fsrs_memory.unwrap_or(MemoryState { - difficulty: 0.0, - stability: 0.0 - })), + memory_to_i64(&stat.fsrs_memory), stat.fsrs_open_day.to_epoch_days() ]) .map_err(|_| DbSaveError)?; diff --git a/src/data_provider/decks.rs b/src/data_provider/decks.rs index b320e57..1a885a7 100644 --- a/src/data_provider/decks.rs +++ b/src/data_provider/decks.rs @@ -4,6 +4,7 @@ use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; use rusqlite::Connection; + pub fn load_sets(connection: &Connection) -> Result, NotificationData> { let mut stmt = connection .prepare("SELECT id, name, forward, backward, filter FROM decks") diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index 86660ec..998b1f0 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -1,3 +1,14 @@ +use crate::core::srs::OrderMode; +use crate::data::Id; +use crate::data::entities::{DeckSettings, EntityData}; +use crate::data::repositories::{BatchedDbLayer, DbLayer}; +use crate::notification::MaybeError; +use crate::notification::NotificationData::{DbLoadError, DbSaveError}; +use log::info; +use rusqlite::{Connection, Params, Row, params}; +use std::marker::PhantomData; +use std::time::Instant; + pub(crate) mod card_states; pub(crate) mod decks; pub(crate) mod history; @@ -7,3 +18,112 @@ pub(crate) mod sqlite; pub(crate) mod voice; pub(crate) mod web_api; pub(crate) mod words; + +pub trait TableAccessQuerySet { + fn load() -> &'static str; + fn save() -> &'static str; + fn add() -> &'static str; + fn name() -> &'static str; + fn from_sql(row: &Row) -> T; + fn insert_params(entity: &T) -> impl Params; + fn update_params(entity: &T) -> impl Params; +} + +pub struct SqliteDbLayer +where + Q: TableAccessQuerySet, +{ + query_set: Q, + _phantom: PhantomData, +} + +impl> DbLayer for SqliteDbLayer { + fn write_entity(&self, entity: &T, connection: &Connection) -> MaybeError { + connection + .execute(Q::load(), Q::update_params(entity)) + .map_err(|_| DbSaveError)?; + Ok(()) + } + + fn add_entity(&mut self, entity: &mut T, connection: &Connection) -> MaybeError { + let index: u32 = connection + .query_row(Q::add(), Q::insert_params(entity), |row| row.get(0)) + .map_err(|_| DbSaveError)?; + + entity.set_id(index.into()); + Ok(()) + } + + fn delete_entity(&mut self, id: Id, connection: &Connection) -> MaybeError { + if !id.is_valid() { + return Err(DbSaveError); + } + connection + .execute( + format!("DELETE FROM {} WHERE id = ?1", Q::name()).as_str(), + (&id,), + ) + .map_err(|_| DbSaveError)?; + Ok(()) + } + + fn load_all(&self, storage: &mut Vec, connection: &Connection) -> MaybeError { + let mut stmt = connection + .prepare(Q::load()) + .expect("Failed to prepare state query"); + let iter = stmt + .query_map([], |row| Ok(Q::from_sql(row))) + .map_err(|_| DbLoadError)?; + + iter.filter_map(|res| res.ok()) + .for_each(|i| storage.push(i)); + Ok(()) + } +} + +impl> BatchedDbLayer for SqliteDbLayer { + fn delete_many_entities(&mut self, ids: &[Id], connection: &mut Connection) -> MaybeError { + for chunk in ids.chunks(1000) { + let stringed = chunk + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(","); + connection + .execute( + format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(), + (), + ) + .map_err(|_| DbSaveError)?; + } + Ok(()) + } + + fn add_many_entities(&mut self, entities: &mut [T], connection: &mut Connection) -> MaybeError { + let time = Instant::now(); + let tx = connection + .transaction() + .expect("Failed to create transaction"); + let count = entities.len(); + + { + let mut stmt = tx.prepare(Q::add()).expect("Failed to prepare state query"); + for entity in entities.iter() { + stmt.execute(Q::insert_params(entity)) + .map_err(|_| DbSaveError)?; + } + } + + let last_index: u32 = tx.last_insert_rowid() as u32; + tx.commit().expect("Failed to commit"); + + let start_index = last_index - (count as u32) + 1; + + for (index, id) in (start_index..=last_index).enumerate() { + entities[index].set_id(id.into()); + } + + info!("Added {} entities for {:?}", entities.len(), time.elapsed()); + Ok(()) + } +} diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 32a7bc4..c885e9c 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -65,4 +65,10 @@ pub fn default_connection() -> Connection { .execute("PRAGMA foreign_keys = ON;", []) .expect("Failed to set PRAGMA"); connection + .execute("PRAGMA journal_mode = WAL;", []) + .expect("Failed to set PRAGMA"); + connection + .execute("PRAGMA synchronous = NORMAL;", []) + .expect("Failed to set PRAGMA"); + connection }