Skip to main content

assetpack_core/sqlite/
mod.rs

1mod common;
2#[cfg(feature = "sqlite-pack")]
3mod pack;
4#[cfg(any(feature = "sqlite-pack", feature = "rusqlite-store"))]
5mod rusqlite;
6#[cfg(feature = "sqlx-store")]
7mod sqlx;
8mod statements;
9
10pub use common::{MerkleMode, MerkleProof, MerkleSummary, SqliteStoreOptions};
11pub(in crate::sqlite) use common::{
12  SqliteLayout, StoredObjectRow, codec_to_str, hash_pair, merkle_root_from_hashes, object_record_from_row, prepare_object,
13  schema_statements, verified_object_from_row,
14};
15#[cfg(feature = "sqlite-pack")]
16pub use pack::{SqlitePack, SqlitePackJournalMode};
17#[cfg(any(feature = "sqlite-pack", feature = "rusqlite-store"))]
18pub use rusqlite::RusqliteStore;
19#[cfg(feature = "sqlx-store")]
20pub use sqlx::SqlxStore;
21
22#[cfg(all(test, feature = "rusqlite-store"))]
23mod tests {
24  use super::{RusqliteStore, SqliteStoreOptions};
25  use crate::{Codec, Hash32, ObjectKind, ObjectRecord, ObjectSource};
26
27  fn record(bytes: &[u8]) -> ObjectRecord {
28    ObjectRecord {
29      hash: Hash32::sha3_256(bytes),
30      kind: ObjectKind::Chunk,
31      decoded_len: bytes.len() as u64,
32      codec: Codec::Raw,
33      stored_bytes: bytes.to_vec(),
34    }
35  }
36
37  #[test]
38  fn rusqlite_store_joins_host_commit_and_rollback() {
39    let mut connection = rusqlite::Connection::open_in_memory().unwrap();
40    connection
41      .execute("CREATE TABLE app_history (id INTEGER PRIMARY KEY, label TEXT NOT NULL)", [])
42      .unwrap();
43    RusqliteStore::ensure_schema(&connection).unwrap();
44    let object = record(b"transaction-object");
45
46    {
47      let transaction = connection.transaction().unwrap();
48      transaction
49        .execute("INSERT INTO app_history (label) VALUES ('rollback')", [])
50        .unwrap();
51      RusqliteStore::from_connection(&transaction)
52        .unwrap()
53        .put_objects_batch(std::slice::from_ref(&object))
54        .unwrap();
55      transaction.rollback().unwrap();
56    }
57    assert_eq!(
58      connection
59        .query_row("SELECT COUNT(*) FROM app_history", [], |row| row.get::<_, i64>(0))
60        .unwrap(),
61      0
62    );
63    assert!(
64      RusqliteStore::from_connection(&connection)
65        .unwrap()
66        .read_object(&object.hash)
67        .unwrap()
68        .is_none()
69    );
70
71    {
72      let transaction = connection.transaction().unwrap();
73      transaction
74        .execute("INSERT INTO app_history (label) VALUES ('commit')", [])
75        .unwrap();
76      RusqliteStore::from_connection(&transaction)
77        .unwrap()
78        .put_objects_batch(std::slice::from_ref(&object))
79        .unwrap();
80      transaction.commit().unwrap();
81    }
82    assert_eq!(
83      connection
84        .query_row("SELECT COUNT(*) FROM app_history", [], |row| row.get::<_, i64>(0))
85        .unwrap(),
86      1
87    );
88    assert_eq!(
89      RusqliteStore::from_connection(&connection)
90        .unwrap()
91        .read_object(&object.hash)
92        .unwrap()
93        .unwrap()
94        .bytes,
95      b"transaction-object"
96    );
97  }
98
99  #[cfg(feature = "sqlx-store")]
100  #[tokio::test]
101  async fn sqlite_schema_and_objects_roundtrip_across_drivers_with_prefix() {
102    use sqlx::{Row, sqlite::SqliteConnectOptions};
103
104    use super::SqlxStore;
105    use crate::AsyncObjectSource;
106
107    let directory = tempfile::tempdir().unwrap();
108    let path = directory.path().join("cross-driver.db");
109    let first = record(b"written-by-rusqlite");
110    {
111      let connection = rusqlite::Connection::open(&path).unwrap();
112      let store = RusqliteStore::from_connection_with_options(
113        &connection,
114        SqliteStoreOptions {
115          namespace: Some("asset".into()),
116          ..Default::default()
117        },
118      )
119      .unwrap();
120      store.put_objects_batch(std::slice::from_ref(&first)).unwrap();
121      let summary = store.rebuild_merkle_index().unwrap();
122      assert_eq!(summary.leaf_count, 1);
123    }
124
125    let pool = sqlx::SqlitePool::connect_with(SqliteConnectOptions::new().filename(&path))
126      .await
127      .unwrap();
128    let store = SqlxStore::from_pool_with_options(
129      pool.clone(),
130      SqliteStoreOptions {
131        namespace: Some("asset".into()),
132        ..Default::default()
133      },
134    )
135    .await
136    .unwrap();
137    assert_eq!(store.read_object(&first.hash).await.unwrap().unwrap().bytes, b"written-by-rusqlite");
138    let schema = sqlx::query("SELECT name FROM sqlite_master WHERE type IN ('table', 'index') AND name LIKE 'asset_%' ORDER BY name")
139      .fetch_all(&pool)
140      .await
141      .unwrap()
142      .into_iter()
143      .map(|row| row.get::<String, _>(0))
144      .collect::<Vec<_>>();
145    assert_eq!(
146      schema,
147      [
148        "asset_file_recipe_cache",
149        "asset_merkle_leaves",
150        "asset_merkle_meta",
151        "asset_merkle_nodes",
152        "asset_objects",
153      ]
154    );
155    let second = record(b"written-by-sqlx");
156    let mut transaction = pool.begin().await.unwrap();
157    store
158      .put_objects_batch_tx(&mut transaction, std::slice::from_ref(&second))
159      .await
160      .unwrap();
161    transaction.commit().await.unwrap();
162    pool.close().await;
163
164    let connection = rusqlite::Connection::open(&path).unwrap();
165    let store = RusqliteStore::from_connection_with_options(
166      &connection,
167      SqliteStoreOptions {
168        namespace: Some("asset".into()),
169        ..Default::default()
170      },
171    )
172    .unwrap();
173    assert_eq!(store.read_object(&second.hash).unwrap().unwrap().bytes, b"written-by-sqlx");
174    assert_eq!(store.merkle_summary().unwrap().leaf_count, 1);
175  }
176}