implement permanent storage for profile history

This commit is contained in:
yggverse 2025-03-12 16:16:16 +02:00
parent 7803aa1c44
commit a6107bf1bb
8 changed files with 310 additions and 73 deletions

View file

@ -63,7 +63,8 @@ pub fn init(tx: &Transaction) -> Result<usize> {
`request` TEXT NOT NULL,
`title` TEXT NULL,
FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`)
FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`),
UNIQUE (`profile_id`, `request`)
)",
[],
)?)

View file

@ -1,14 +1,17 @@
// mod database;
mod database;
mod item;
mod memory;
use anyhow::Result;
use database::Database;
use gtk::glib::GString;
use item::Item;
use item::{Event, Item};
use memory::Memory;
use sqlite::Connection;
use sqlite::{Connection, Transaction};
use std::{cell::RefCell, rc::Rc, sync::RwLock};
pub struct History {
database: Database, // permanent storage
memory: RefCell<Memory>, // fast search index
}
@ -16,12 +19,35 @@ impl History {
// Constructors
/// Create new `Self`
pub fn build(_connection: &Rc<RwLock<Connection>>, _profile_id: &Rc<i64>) -> Self {
pub fn build(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Result<Self> {
// Init children components
let database = Database::build(connection, profile_id);
let memory = RefCell::new(Memory::new());
for item in database.records(None, None)? {
memory.borrow_mut().add(item)
}
// Return new `Self`
Self { memory }
Ok(Self { database, memory })
}
// Actions
pub fn save(&self) -> Result<()> {
for item in self.memory.borrow().items() {
if !item.is_saved {
match item.id {
Some(_) => {
self.database.update(item)?;
}
None => {
self.database.add(item)?;
}
}
}
}
Ok(())
}
// Actions
@ -30,7 +56,14 @@ impl History {
pub fn open(&self, request: GString, title: Option<GString>) {
let mut memory = self.memory.borrow_mut();
if !memory.open(&request) {
memory.add(Item::init(request, title))
memory.add(Item {
id: None,
request,
title,
opened: Event::new(),
closed: None,
is_saved: false,
})
}
}
@ -56,3 +89,16 @@ impl History {
self.memory.borrow().contains_request(request, limit)
}
}
// Tools
pub fn migrate(tx: &Transaction) -> Result<()> {
// Migrate self components
database::init(tx)?;
// Delegate migration to childs
// nothing yet..
// Success
Ok(())
}

View file

@ -0,0 +1,183 @@
use super::{item::Event, Item};
use anyhow::Result;
use gtk::glib::DateTime;
use sqlite::{Connection, Transaction};
use std::{rc::Rc, sync::RwLock};
pub struct Database {
connection: Rc<RwLock<Connection>>,
profile_id: Rc<i64>, // multi-profile relationship
}
impl Database {
// Constructors
/// Create new `Self`
pub fn build(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Self {
Self {
connection: connection.clone(),
profile_id: profile_id.clone(),
}
}
// Getters
/// Get history records from database with optional filter by `request`
pub fn records(&self, request: Option<&str>, title: Option<&str>) -> Result<Vec<Item>> {
let readable = self.connection.read().unwrap(); // @TODO
let tx = readable.unchecked_transaction()?;
select(&tx, *self.profile_id, request, title)
}
// Actions
/// Create new history record in database
/// * return last insert ID on success
pub fn add(&self, item: &Item) -> Result<i64> {
let mut writable = self.connection.write().unwrap(); // @TODO
let tx = writable.transaction()?;
let id = insert(&tx, *self.profile_id, item)?;
tx.commit()?;
Ok(id)
}
pub fn update(&self, item: &Item) -> Result<usize> {
let mut writable = self.connection.write().unwrap(); // @TODO
let tx = writable.transaction()?;
let affected = update(&tx, *self.profile_id, item)?;
tx.commit()?;
Ok(affected)
}
}
// Low-level DB API
pub fn init(tx: &Transaction) -> Result<usize> {
Ok(tx.execute(
"CREATE TABLE IF NOT EXISTS `profile_history`
(
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`profile_id` INTEGER NOT NULL,
`opened_time` INTEGER NOT NULL,
`opened_count` INTEGER NOT NULL,
`closed_time` INTEGER NULL,
`closed_count` INTEGER NULL,
`request` TEXT NOT NULL,
`title` TEXT NULL,
FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`),
UNIQUE (`profile_id`, `request`)
)",
[],
)?)
}
pub fn insert(tx: &Transaction, profile_id: i64, item: &Item) -> Result<i64> {
tx.execute(
"INSERT INTO `profile_history` (
`profile_id`,
`opened_time`,
`opened_count`,
`closed_time`,
`closed_count`,
`request`,
`title`
) VALUES (?, ?, ?, ?, ?, ?, ?)",
(
profile_id,
item.opened.time.to_unix(),
item.opened.count as i64,
item.closed.as_ref().map(|closed| closed.time.to_unix()),
item.closed.as_ref().map(|closed| closed.count as i64),
item.request.as_str(),
item.title.as_deref(),
),
)?;
Ok(tx.last_insert_rowid())
}
pub fn update(tx: &Transaction, profile_id: i64, item: &Item) -> Result<usize> {
Ok(tx.execute(
"UPDATE `profile_history`
SET `opened_time` = ?,
`opened_count` = ?,
`closed_time` = ?,
`closed_count` = ?,
`request` = ?,
`title` = ?
WHERE `id` = ? AND `profile_id` = ?",
(
item.opened.time.to_unix(),
item.opened.count as i64,
item.closed.as_ref().map(|closed| closed.time.to_unix()),
item.closed.as_ref().map(|closed| closed.count as i64),
item.request.as_str(),
item.title.as_deref(),
item.id.unwrap(),
profile_id,
),
)?)
}
pub fn select(
tx: &Transaction,
profile_id: i64,
request: Option<&str>,
title: Option<&str>,
) -> Result<Vec<Item>> {
let mut stmt = tx.prepare(
"SELECT
`id`,
`profile_id`,
`opened_time`,
`opened_count`,
`closed_time`,
`closed_count`,
`request`,
`title`
FROM `profile_history`
WHERE `profile_id` = ? AND (`request` LIKE ? OR `title` LIKE ?)",
)?;
let result = stmt.query_map(
(profile_id, request.unwrap_or("%"), title.unwrap_or("%")),
|row| {
Ok(Item {
id: row.get(0)?,
//profile_id: row.get(1)?,
opened: Event {
time: DateTime::from_unix_local(row.get(2)?).unwrap(),
count: row.get(3)?,
},
closed: closed(row.get(4)?, row.get(5)?),
request: row.get::<_, String>(6)?.into(),
title: row.get::<_, Option<String>>(7)?.map(|s| s.into()),
is_saved: true,
})
},
)?;
let mut items = Vec::new();
for record in result {
let item = record?;
items.push(item);
}
Ok(items)
}
// Tools
fn closed(time: Option<i64>, count: Option<i64>) -> Option<Event> {
if let Some(t) = time {
if let Some(c) = count {
return Some(Event {
time: DateTime::from_unix_local(t).unwrap(),
count: c as usize,
});
}
panic!()
}
None
}

View file

@ -1,6 +1,6 @@
mod event;
pub mod event;
pub use event::Event;
use event::Event;
use gtk::glib::GString;
#[derive(Clone)]
@ -18,25 +18,17 @@ pub struct Item {
/// Collect `Item` close events
/// * used in recently closed pages menu and history page
pub closed: Option<Event>,
/// Mark in-memory `Item` as saved
/// * used for database update (e.g. on app close)
pub is_saved: bool,
}
impl Item {
// Constructors
pub fn init(request: GString, title: Option<GString>) -> Self {
Self {
id: None,
request,
title,
opened: Event::new(),
closed: None,
}
}
// Actions
pub fn open(&mut self) {
self.opened.pulse()
self.opened.pulse();
self.is_saved = false
}
pub fn close(&mut self) {
@ -44,5 +36,6 @@ impl Item {
Some(ref mut closed) => closed.pulse(),
None => self.closed = Some(Event::new()),
}
self.is_saved = false
}
}

View file

@ -21,9 +21,9 @@ impl Memory {
/// Update `opened` time for given `request`
/// * return `false` if the `request` not found in memory index
pub fn open(&mut self, request: &str) -> bool {
for record in &mut self.0 {
if record.request == request {
record.open();
for item in &mut self.0 {
if item.request == request {
item.open();
return true;
}
}
@ -32,9 +32,9 @@ impl Memory {
/// Update `closed` time for given `request`
pub fn close(&mut self, request: &str) {
for record in &mut self.0 {
if record.request == request {
record.close();
for item in &mut self.0 {
if item.request == request {
item.close();
return;
}
}
@ -102,6 +102,10 @@ impl Memory {
}
items
}
pub fn items(&self) -> &Vec<Item> {
&self.0
}
}
impl Default for Memory {