diff --git a/atuin-client/record-migrations/20230531212437_create-records.sql b/atuin-client/record-migrations/20230531212437_create-records.sql index 120a9eac..fae20cc0 100644 --- a/atuin-client/record-migrations/20230531212437_create-records.sql +++ b/atuin-client/record-migrations/20230531212437_create-records.sql @@ -6,5 +6,9 @@ create table if not exists records ( tag text not null, version text not null, - data blob not null, + data blob not null ); + +create index host_idx on records (host); +create index tag_idx on records (tag); +create index host_tag_idx on records (host, tag); diff --git a/atuin-client/src/record/sqlite_store.rs b/atuin-client/src/record/sqlite_store.rs index 8ee195a9..5aa144aa 100644 --- a/atuin-client/src/record/sqlite_store.rs +++ b/atuin-client/src/record/sqlite_store.rs @@ -2,23 +2,15 @@ // Multiple stores of multiple types are all stored in one chonky table (for now), and we just index // by tag/host -yo tomorrow morning me -drink that coffee -then wrap up this interface - -you will need to -- make sure the records use string IDs -- add the version in -- write some tests with a memory sqlite - use std::path::Path; use std::str::FromStr; use async_trait::async_trait; +use eyre::Result; use fs_err as fs; use sqlx::{ sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow}, - Result, Row, + Row, }; use atuin_common::record::Record; @@ -60,11 +52,94 @@ impl SqliteStore { Ok(()) } + + async fn save_raw(tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, r: &Record) -> Result<()> { + // In sqlite, we are "limited" to i64. But that is still fine, until 2262. + sqlx::query( + "insert or ignore into records(id, host, timestamp, tag, version, data) + values(?1, ?2, ?3, ?4, ?5, ?6)", + ) + .bind(r.id.as_str()) + .bind(r.host.as_str()) + .bind(r.timestamp as i64) + .bind(r.tag.as_str()) + .bind(r.version.as_str()) + .bind(r.data.as_slice()) + .execute(tx) + .await?; + + Ok(()) + } + + fn query_row(row: SqliteRow) -> Record { + let timestamp: i64 = row.get("timestamp"); + + Record { + id: row.get("id"), + host: row.get("host"), + timestamp: timestamp as u64, + tag: row.get("tag"), + version: row.get("version"), + data: row.get("data"), + } + } } #[async_trait] impl Store for SqliteStore { - async fn push(record: Record) -> Result { + async fn push(&self, record: Record) -> Result { + // TODO: batch inserts + let mut tx = self.pool.begin().await?; + Self::save_raw(&mut tx, &record).await?; + Ok(record) } + + async fn get(&self, id: String) -> Result { + let res = sqlx::query("select * from records where id = ?1") + .bind(id.as_str()) + .map(Self::query_row) + .fetch_one(&self.pool) + .await?; + + Ok(res) + } + + async fn len(&self, host: String, tag: String) -> Result { + let res: (i64,) = + sqlx::query_as("select count(1) from records where host = ?1 and tag = ?2") + .bind(host.as_str()) + .bind(tag.as_str()) + .fetch_one(&self.pool) + .await?; + + Ok(res.0 as u64) + } +} + +#[cfg(test)] +mod tests { + use super::SqliteStore; + + #[tokio::test] + async fn create_db() { + let db = SqliteStore::new(":memory:").await; + + assert!( + db.is_ok(), + "db could not be created, {:?}", + db.err().unwrap() + ); + } + + #[tokio::test] + async fn push_record() { + let db = SqliteStore::new(":memory:").await; + + assert!( + db.is_ok(), + "db could not be created, {:?}", + db.err().unwrap() + ); + } } diff --git a/atuin-client/src/record/store.rs b/atuin-client/src/record/store.rs index 04a266f8..ab34289d 100644 --- a/atuin-client/src/record/store.rs +++ b/atuin-client/src/record/store.rs @@ -9,7 +9,7 @@ use atuin_common::record::Record; /// be shell history, kvs, etc. #[async_trait] pub trait Store { - async fn push(record: Record) -> Result; - async fn get(id: String) -> Result; - async fn len(host: String, tag: String) -> Result; + async fn push(&self, record: Record) -> Result; + async fn get(&self, id: String) -> Result; + async fn len(&self, host: String, tag: String) -> Result; } diff --git a/atuin-common/src/record.rs b/atuin-common/src/record.rs index 514ec786..fe4d7eef 100644 --- a/atuin-common/src/record.rs +++ b/atuin-common/src/record.rs @@ -1,13 +1,15 @@ /// A single record stored inside of our local database pub struct Record { - pub id: i64, + pub id: String, pub host: String, pub timestamp: u64, - /// The type of data we are storing here. It is probably useful to also - /// include some sort of version. For example, history.v2 + // However we want to track versions for this tag, eg v2 + pub version: String, + + /// The type of data we are storing here. Eg, "history" pub tag: String, /// Some data. This can be anything you wish to store. Use the tag field to know how to handle it.