From eb78dad91aad89f3a01e88207adbf0e0e5b0c2c2 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 20 Aug 2022 12:42:20 +0000 Subject: [PATCH] Replace FS with Sqlite (#20) - [x] Deps: add rusqlite - [x] Deps: add time - [x] Add created_at to the domain Task - [x] Add schema and first migration - [x] Check current version and run migration - [x] Implement Repository trait - [x] Add script to migrate from FS implementation Closes #5 Closes #19 Reviewed-on: https://git.pleshevski.ru/pleshevskiy/tas/pulls/20 Co-authored-by: Dmitriy Pleshevskiy Co-committed-by: Dmitriy Pleshevskiy --- Cargo.lock | 114 ++++++++++ Cargo.toml | 5 + database/migrations/202208162308.sql | 26 +++ database/schema.sql | 26 +++ src/bin/fs_to_sqlite.rs | 26 +++ src/domain.rs | 4 +- src/lib.rs | 2 + src/main.rs | 11 +- src/repo.rs | 19 +- src/repo/fs.rs | 9 +- src/repo/sqlite.rs | 316 +++++++++++++++++++++++++++ 11 files changed, 546 insertions(+), 12 deletions(-) create mode 100644 database/migrations/202208162308.sql create mode 100644 database/schema.sql create mode 100644 src/bin/fs_to_sqlite.rs create mode 100644 src/lib.rs create mode 100644 src/repo/sqlite.rs diff --git a/Cargo.lock b/Cargo.lock index 26cbaf5..1c399dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -14,6 +25,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfg-if" version = "1.0.0" @@ -76,6 +93,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "getrandom" version = "0.2.7" @@ -92,6 +121,18 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -121,6 +162,17 @@ version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" +[[package]] +name = "libsqlite3-sys" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "log" version = "0.4.17" @@ -130,6 +182,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.13.0" @@ -142,6 +203,12 @@ version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -204,6 +271,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rusqlite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "time", +] + [[package]] name = "ryu" version = "1.0.11" @@ -241,6 +323,12 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + [[package]] name = "syn" version = "1.0.99" @@ -258,8 +346,10 @@ version = "0.1.0" dependencies = [ "clap", "log", + "rusqlite", "serde", "serde_json", + "time", "xdg", ] @@ -289,12 +379,36 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "unicode-ident" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 0c82a22..7f45445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ repository = "https://git.pleshevski.ru/pleshevskiy/tas" version = "0.1.0" edition = "2021" license = "GPL-3.0+" +default-run = "tas" [badges] maintenance = { status = "experimental" } @@ -12,6 +13,10 @@ maintenance = { status = "experimental" } [dependencies] clap = { version = "3.2.16", default-features = false, features = ["derive", "std"] } log = "0.4.17" +rusqlite = { version = "0.28.0", features = ["bundled", "time"] } serde = { version = "1.0.142", features = ["derive"] } serde_json = "1.0.83" +time = "0.3" xdg = "2.4.1" + + diff --git a/database/migrations/202208162308.sql b/database/migrations/202208162308.sql new file mode 100644 index 0000000..981c7bb --- /dev/null +++ b/database/migrations/202208162308.sql @@ -0,0 +1,26 @@ +CREATE TABLE _tas_info ( + version INTEGER PRIMARY KEY +); + +CREATE TABLE tasks ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + project TEXT , + link TEXT , + dir_path TEXT , + + current BOOLEAN NOT NULL DEFAULT false, + + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + finished_at DATETIME +); + +CREATE VIEW active_tasks +AS +SELECT + t.*, + row_number() OVER (ORDER BY t.created_at) AS idx +FROM tasks AS t +WHERE t.finished_at IS NULL +ORDER BY t.created_at +; diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 0000000..981c7bb --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,26 @@ +CREATE TABLE _tas_info ( + version INTEGER PRIMARY KEY +); + +CREATE TABLE tasks ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + project TEXT , + link TEXT , + dir_path TEXT , + + current BOOLEAN NOT NULL DEFAULT false, + + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + finished_at DATETIME +); + +CREATE VIEW active_tasks +AS +SELECT + t.*, + row_number() OVER (ORDER BY t.created_at) AS idx +FROM tasks AS t +WHERE t.finished_at IS NULL +ORDER BY t.created_at +; diff --git a/src/bin/fs_to_sqlite.rs b/src/bin/fs_to_sqlite.rs new file mode 100644 index 0000000..3501306 --- /dev/null +++ b/src/bin/fs_to_sqlite.rs @@ -0,0 +1,26 @@ +use tas::repo::{self, Repository}; +use xdg::BaseDirectories; + +fn main() { + let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap(); + let fs_repo = repo::fs::FsRepo::new(xdg_dirs.clone()); + let tasks = fs_repo.get_tasks().unwrap(); + + let sqlite_repo = repo::sqlite::SqliteRepo::new(xdg_dirs).unwrap(); + for task in tasks { + log::info!("task: {}", task.name); + log::info!(" inserting..."); + + sqlite_repo + .insert_task(repo::InsertTaskData { + name: task.name, + project: task.project, + link: task.link, + dir_path: task.dir_path, + index: None, + }) + .unwrap(); + + log::info!(" inserted"); + } +} diff --git a/src/domain.rs b/src/domain.rs index 45ca64b..bcdbb1e 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -15,14 +15,14 @@ //! use std::path::PathBuf; -pub type TaskId = usize; +pub type TaskIdx = usize; pub struct Task { pub name: String, pub project: Option, pub link: Option, pub dir_path: Option, - // created_at + pub created_at: time::OffsetDateTime, } pub struct CurrentTaskInfo { diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7435596 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod domain; +pub mod repo; diff --git a/src/main.rs b/src/main.rs index 86c5c15..1ee17d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,7 +27,7 @@ //--------------------------------------------------------------------- use clap::Parser; -use repo::fs::FsRepo; +use repo::sqlite::SqliteRepo; use xdg::BaseDirectories; mod cli; @@ -38,7 +38,14 @@ fn main() { let args = cli::Args::parse(); let xdg_dirs = BaseDirectories::with_prefix(env!("CARGO_PKG_NAME")).unwrap(); - let repo = FsRepo::new(xdg_dirs); + let repo = match SqliteRepo::new(xdg_dirs) { + Ok(repo) => repo, + Err(err) => return eprintln!("Cannot connect to repository: {err}"), + }; + + if let Err(err) = repo.upgrade() { + return eprintln!("Cannot upgrade database: {err}"); + } match args.command { cli::SubCommand::Add(args) => { diff --git a/src/repo.rs b/src/repo.rs index 6c6ca09..50f66cc 100755 --- a/src/repo.rs +++ b/src/repo.rs @@ -14,6 +14,7 @@ //! along with tas. If not, see . //! pub mod fs; +pub mod sqlite; use std::path::PathBuf; @@ -21,17 +22,27 @@ use crate::domain; #[derive(Debug)] pub enum Error { + Connect, + PrepareQuery, + QueryData, NotFound, InvalidData, InsertData, + UpdateData, + RemoveData, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Error::Connect => f.write_str("Cannot connect to the repository"), + Error::PrepareQuery => f.write_str("Cannot prepare query"), + Error::QueryData => f.write_str("Cannot query data"), Error::NotFound => f.write_str("Cannot find data"), Error::InvalidData => f.write_str("Invalid data format"), Error::InsertData => f.write_str("Cannot insert data"), + Error::UpdateData => f.write_str("Cannot update data"), + Error::RemoveData => f.write_str("Cannot remove data"), } } } @@ -56,21 +67,21 @@ pub struct UpdateTaskData { pub trait Repository { fn get_current_task_opt(&self) -> Result, Error>; - fn get_task_opt(&self, id: domain::TaskId) -> Result, Error>; + fn get_task_opt(&self, id: domain::TaskIdx) -> Result, Error>; fn get_tasks(&self) -> Result, Error>; - fn remove_task(&self, id: domain::TaskId) -> Result; + fn remove_task(&self, id: domain::TaskIdx) -> Result; fn update_task( &self, - id: domain::TaskId, + id: domain::TaskIdx, update_data: UpdateTaskData, ) -> Result; fn insert_task(&self, insert_data: InsertTaskData) -> Result; - fn start_task(&self, id: domain::TaskId) -> Result; + fn start_task(&self, id: domain::TaskIdx) -> Result; fn stop_task(&self) -> Result; diff --git a/src/repo/fs.rs b/src/repo/fs.rs index 86811ef..55f83fc 100644 --- a/src/repo/fs.rs +++ b/src/repo/fs.rs @@ -39,6 +39,7 @@ impl From for domain::Task { project: repo.group, link: repo.link, dir_path: repo.path.map(PathBuf::from), + created_at: time::OffsetDateTime::now_utc(), } } } @@ -79,7 +80,7 @@ impl Repository for FsRepo { .map(|cur_task| cur_task.map(From::from)) } - fn get_task_opt(&self, id: domain::TaskId) -> Result, Error> { + fn get_task_opt(&self, id: domain::TaskIdx) -> Result, Error> { let tasks = self.get_tasks_impl()?; if id == 0 || id > tasks.len() { return Err(Error::NotFound); @@ -93,7 +94,7 @@ impl Repository for FsRepo { .map(|tasks| tasks.into_iter().map(Task::into).collect()) } - fn remove_task(&self, id: domain::TaskId) -> Result { + fn remove_task(&self, id: domain::TaskIdx) -> Result { let mut tasks = self.get_tasks_impl()?; if id == 0 || id > tasks.len() { return Err(Error::NotFound); @@ -107,7 +108,7 @@ impl Repository for FsRepo { fn update_task( &self, - id: domain::TaskId, + id: domain::TaskIdx, update_data: UpdateTaskData, ) -> Result { let mut tasks = self.get_tasks_impl()?; @@ -161,7 +162,7 @@ impl Repository for FsRepo { Ok(new_task.into()) } - fn start_task(&self, id: domain::TaskId) -> Result { + fn start_task(&self, id: domain::TaskIdx) -> Result { let tasks = self.get_tasks_impl()?; if id == 0 || id > tasks.len() { return Err(Error::NotFound); diff --git a/src/repo/sqlite.rs b/src/repo/sqlite.rs new file mode 100644 index 0000000..ba299f1 --- /dev/null +++ b/src/repo/sqlite.rs @@ -0,0 +1,316 @@ +use std::path::PathBuf; + +use rusqlite::{Connection, OptionalExtension}; +use xdg::BaseDirectories; + +use crate::domain; +use crate::repo::{Error, Repository}; + +struct Task { + id: i64, + name: String, + project: Option, + link: Option, + dir_path: Option, + /* + current: bool, + finished_at: Option, + */ + created_at: time::OffsetDateTime, + + idx: i64, +} + +impl From for domain::Task { + fn from(repo: Task) -> Self { + Self { + name: repo.name, + project: repo.project, + link: repo.link, + dir_path: repo.dir_path.map(PathBuf::from), + created_at: repo.created_at, + } + } +} + +impl<'r> TryFrom<&'r rusqlite::Row<'_>> for Task { + type Error = rusqlite::Error; + + fn try_from(row: &'r rusqlite::Row<'_>) -> Result { + Ok(Self { + id: row.get("id")?, + name: row.get("name")?, + project: row.get("project")?, + link: row.get("link")?, + dir_path: row.get("dir_path")?, + /* + current: row.get("current")?, + finished_at: row.get("finished_at")?, + */ + created_at: row.get("created_at")?, + idx: row.get("idx")?, + }) + } +} + +const SCHEMA_FILE: &str = "schema.sql"; + +pub struct SqliteRepo { + conn: Connection, +} + +impl SqliteRepo { + pub fn new(xdg_dirs: BaseDirectories) -> Result { + let file_path = xdg_dirs.get_data_file(SCHEMA_FILE); + let conn = Connection::open(file_path).map_err(|_| Error::Connect)?; + + Ok(Self { conn }) + } +} + +impl Repository for SqliteRepo { + fn get_current_task_opt(&self) -> Result, Error> { + let db_task = self.get_current_task_opt_impl()?; + + Ok(db_task.map(|db_task| domain::CurrentTaskInfo { + task_idx: db_task.idx as usize, + task: db_task.into(), + })) + } + + fn get_task_opt(&self, id: domain::TaskIdx) -> Result, Error> { + self.get_task_opt_impl(id).map(|t| t.map(From::from)) + } + + fn get_tasks(&self) -> Result, Error> { + let mut stmt = self + .conn + .prepare("SELECT * FROM active_tasks") + .map_err(|_| Error::PrepareQuery)?; + + let rows = stmt + .query_map([], |row| Task::try_from(row)) + .map_err(|_| Error::QueryData)?; + + rows.into_iter() + .map(|db_task| db_task.map(From::from).map_err(|_| Error::QueryData)) + .collect::, Error>>() + .map_err(|_| Error::InvalidData) + } + + fn remove_task(&self, idx: domain::TaskIdx) -> Result { + let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?; + + let mut stmt = self + .conn + .prepare("DELETE FROM tasks WHERE id = ?") + .map_err(|_| Error::PrepareQuery)?; + + stmt.execute([&(db_task.id)]) + .map_err(|_| Error::RemoveData)?; + + Ok(db_task.into()) + } + + fn insert_task(&self, insert_data: super::InsertTaskData) -> Result { + let mut stmt = self + .conn + .prepare( + "INSERT INTO tasks (name, project, link, dir_path) + VALUES (?1, ?2, ?3, ?4)", + ) + .map_err(|_| Error::PrepareQuery)?; + + let id = stmt + .insert(( + &insert_data.name, + &insert_data.project, + &insert_data.link, + &insert_data + .dir_path + .and_then(|p| p.into_os_string().into_string().ok()), + )) + .map_err(|_| Error::InsertData)?; + + self.get_task_by_id_impl(id).map(From::from) + } + + fn update_task( + &self, + idx: domain::TaskIdx, + update_data: super::UpdateTaskData, + ) -> Result { + let task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?; + + let mut stmt = self + .conn + .prepare( + "UPDATE tasks + SET name = ?1, + project = ?2, + link = ?3, + dir_path = ?4 + WHERE id = ?5 + ", + ) + .map_err(|_| Error::PrepareQuery)?; + + stmt.execute(( + &update_data.name.unwrap_or(task.name), + &update_data.project.unwrap_or(task.project), + &update_data.link.unwrap_or(task.link), + &update_data + .dir_path + .unwrap_or(task.dir_path.map(PathBuf::from)) + .and_then(|p| p.into_os_string().into_string().ok()), + &task.id, + )) + .map_err(|_| Error::UpdateData)?; + + self.get_task_opt(idx)?.ok_or(Error::NotFound) + } + + fn start_task(&self, idx: domain::TaskIdx) -> Result { + let db_task = self.get_task_opt_impl(idx)?.ok_or(Error::NotFound)?; + + self.conn + .execute( + "UPDATE tasks SET current = true WHERE id = ?1", + [&db_task.id], + ) + .map_err(|_| Error::UpdateData)?; + + Ok(db_task.into()) + } + + fn stop_task(&self) -> Result { + let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?; + + self.conn + .execute( + "UPDATE tasks SET current = false WHERE id = ?1", + [&db_task.id], + ) + .map_err(|_| Error::UpdateData)?; + + Ok(db_task.into()) + } + + fn finish_task(&self) -> Result { + let db_task = self.get_current_task_opt_impl()?.ok_or(Error::NotFound)?; + + self.conn + .execute( + "UPDATE tasks + SET current = false, + finished_at = CURRENT_TIMESTAMP + WHERE id = ?1", + [&db_task.id], + ) + .map_err(|_| Error::UpdateData)?; + + Ok(db_task.into()) + } +} + +impl SqliteRepo { + fn get_task_by_id_impl(&self, id: i64) -> Result { + let mut stmt = self + .conn + .prepare("SELECT * FROM active_tasks WHERE id = ?") + .map_err(|_| Error::PrepareQuery)?; + + stmt.query_row([id], |row| Task::try_from(row)) + .map_err(|_| Error::QueryData) + } + + fn get_task_opt_impl(&self, idx: domain::TaskIdx) -> Result, Error> { + let mut stmt = self + .conn + .prepare("SELECT * FROM active_tasks WHERE idx = ?") + .map_err(|_| Error::PrepareQuery)?; + + stmt.query_row([idx as i64], |row| Task::try_from(row)) + .optional() + .map_err(|_| Error::QueryData) + } + + fn get_current_task_opt_impl(&self) -> Result, Error> { + let mut stmt = self + .conn + .prepare("SELECT * FROM active_tasks WHERE current IS true") + .map_err(|_| Error::PrepareQuery)?; + + let row = stmt + .query_row([], |row| Task::try_from(row)) + .optional() + .map_err(|_| Error::QueryData)?; + + Ok(row) + } +} + +macro_rules! run_migration { + ($this:ident, $ver:ident = $version:expr) => { + $this + .conn + .execute_batch(&format!( + "BEGIN; {} COMMIT;", + include_str!(concat!("../../database/migrations/", $version, ".sql")) + )) + .map_err(|_| MigrationError::Upgrade)?; + + $ver.replace($version); + }; +} + +// TODO: move migration to a separate repository. +#[derive(Debug)] +pub enum MigrationError { + Upgrade, + DeleteInfo, + InsertInfo, +} + +impl std::fmt::Display for MigrationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MigrationError::Upgrade => f.write_str("Cannot upgrade the migration to a new version"), + MigrationError::DeleteInfo => { + f.write_str("Cannot delete the tas info from the database") + } + MigrationError::InsertInfo => { + f.write_str("Cannot insert a new tas information to the database") + } + } + } +} + +impl std::error::Error for MigrationError {} + +impl SqliteRepo { + pub fn upgrade(&self) -> Result<(), MigrationError> { + let mut version = self.version(); + if version.is_none() { + run_migration!(self, version = 202208162308); + } + + self.conn + .execute("DELETE FROM _tas_info", []) + .map_err(|_| MigrationError::DeleteInfo)?; + self.conn + .execute( + "INSERT INTO _tas_info (version) VALUES (?)", + [version.unwrap()], + ) + .map_err(|_| MigrationError::InsertInfo)?; + + Ok(()) + } + + fn version(&self) -> Option { + self.conn + .query_row("SELECT version FROM _tas_info", [], |r| r.get("version")) + .unwrap_or_default() + } +}