1use std::{
2 collections::HashSet,
3 ffi::OsString,
4 fs, io,
5 marker::PhantomData,
6 path::{Path, PathBuf},
7 sync::{Arc, Mutex, PoisonError, RwLock},
8};
9
10use crate::Dataset;
11
12use futures_lite::future::block_on;
16use gix_tempfile::{
17 AutoRemove, ContainingDirectory, Handle,
18 handle::{Writable, persist},
19};
20use r2d2::Pool;
21use sanitize_filename::sanitize;
22use serde::{Serialize, de::DeserializeOwned};
23use turso::{Builder, Connection, Database, Value};
24
25mod de;
26
27use de::from_row_with_columns;
28pub use de::RowError;
31
32pub type Result<T> = core::result::Result<T, SqliteDatasetError>;
34
35#[derive(thiserror::Error, Debug)]
37pub enum SqliteDatasetError {
38 #[error("IO error: {0}")]
40 Io(#[from] io::Error),
41
42 #[error("Sql error: {0}")]
44 Sql(#[from] turso::Error),
45
46 #[error("Serde error: {0}")]
48 Serde(#[from] rmp_serde::encode::Error),
49
50 #[error("Deserialize error: {0}")]
52 Deserialize(#[from] rmp_serde::decode::Error),
53
54 #[error("Row error: {0}")]
56 Row(#[from] RowError),
57
58 #[error("Overwrite flag is set to false and the database file already exists: {0}")]
60 FileExists(PathBuf),
61
62 #[error("Failed to create connection pool: {0}")]
64 ConnectionPool(#[from] r2d2::Error),
65
66 #[error("Could not persist the temporary database file: {0}")]
68 PersistDbFile(#[from] persist::Error<Writable>),
69
70 #[error("{0}")]
72 Other(&'static str),
73}
74
75impl From<&'static str> for SqliteDatasetError {
76 fn from(s: &'static str) -> Self {
77 SqliteDatasetError::Other(s)
78 }
79}
80
81#[derive(Debug)]
118pub struct SqliteDataset<I> {
119 db_file: PathBuf,
120 split: String,
121 conn_pool: Pool<TursoConnectionManager>,
122 columns: Vec<String>,
123 len: usize,
124 select_statement: String,
125 row_serialized: bool,
126 phantom: PhantomData<I>,
127}
128
129impl<I> SqliteDataset<I> {
130 pub fn from_db_file<P: AsRef<Path>>(db_file: P, split: &str) -> Result<Self> {
132 let database = open_database(&db_file, false)?;
134 let conn_pool = Pool::new(TursoConnectionManager { database })?;
135
136 let row_serialized = Self::check_if_row_serialized(&conn_pool, split)?;
138
139 let select_statement = if row_serialized {
141 format!("select item from {split} where row_id = ?")
142 } else {
143 format!("select * from {split} where row_id = ?")
144 };
145
146 let (columns, len) = fetch_columns_and_len(&conn_pool, &select_statement, split)?;
148
149 Ok(SqliteDataset {
150 db_file: db_file.as_ref().to_path_buf(),
151 split: split.to_string(),
152 conn_pool,
153 columns,
154 len,
155 select_statement,
156 row_serialized,
157 phantom: PhantomData,
158 })
159 }
160
161 fn check_if_row_serialized(
165 conn_pool: &Pool<TursoConnectionManager>,
166 split: &str,
167 ) -> Result<bool> {
168 let conn = conn_pool.get()?;
169
170 let columns = block_on(conn.prepare(&format!("select * from {split}")))?.columns();
173
174 let matches = |index: usize, name: &str, ty: &str| {
175 columns[index].name().eq_ignore_ascii_case(name)
176 && columns[index]
177 .decl_type()
178 .is_some_and(|declared| declared.eq_ignore_ascii_case(ty))
179 };
180
181 Ok(columns.len() == 2 && matches(0, "row_id", "integer") && matches(1, "item", "blob"))
182 }
183
184 pub fn db_file(&self) -> PathBuf {
186 self.db_file.clone()
187 }
188
189 pub fn split(&self) -> &str {
191 self.split.as_str()
192 }
193}
194
195impl<I: DeserializeOwned> SqliteDataset<I> {
196 fn item_from_row(&self, row: &turso::Row) -> Result<I> {
198 if self.row_serialized {
199 match row.get_value(0)? {
201 Value::Blob(blob) => Ok(rmp_serde::from_slice::<I>(&blob)?),
202 _ => Err(SqliteDatasetError::Other("expected a blob column")),
203 }
204 } else {
205 Ok(from_row_with_columns::<I>(row, &self.columns)?)
207 }
208 }
209}
210
211impl<I> Dataset<I, SqliteDatasetError> for SqliteDataset<I>
212where
213 I: Clone + Send + Sync + DeserializeOwned,
214{
215 fn get(&self, index: usize) -> Result<I> {
221 assert!(
222 index < self.len,
223 "Index out of bounds for SqliteDataset: {} >= {}",
224 index,
225 self.len,
226 );
227
228 let row_id = (index + 1) as i64;
231
232 let connection = self.conn_pool.get()?;
234
235 let row = block_on(async {
236 let mut statement = connection.prepare_cached(&self.select_statement).await?;
240 statement.query_row([row_id]).await
241 })
242 .map_err(|error| match error {
243 turso::Error::QueryReturnedNoRows => SqliteDatasetError::Other(
246 "no row for this index; row_id values must be contiguous and start at 1",
247 ),
248 error => error.into(),
249 })?;
250
251 self.item_from_row(&row)
252 }
253
254 fn get_many(&self, indexes: Vec<usize>) -> Result<Vec<I>> {
260 if indexes.is_empty() {
261 return Ok(Vec::new());
262 }
263
264 for &index in &indexes {
265 assert!(
266 index < self.len,
267 "Index out of bounds for SqliteDataset: {} >= {}",
268 index,
269 self.len,
270 );
271 }
272
273 let values_clause = vec!["(?, ?)"; indexes.len()].join(", ");
276 let params: Vec<i64> = indexes
277 .iter()
278 .enumerate()
279 .flat_map(|(ord, &index)| [(index + 1) as i64, ord as i64])
280 .collect();
281
282 let split = &self.split;
283 let connection = self.conn_pool.get()?;
284
285 let selection = if self.row_serialized { "t.item" } else { "t.*" };
286 let query = format!(
287 "WITH req(row_id, ord) AS (VALUES {values_clause}) \
288 SELECT {selection} FROM req JOIN {split} t ON t.row_id = req.row_id ORDER BY req.ord"
289 );
290
291 block_on(async {
299 let mut statement = connection.prepare(&query).await?;
300 let mut rows = statement.query(params).await?;
301
302 let mut items = Vec::with_capacity(indexes.len());
303 while let Some(row) = rows.next().await? {
304 items.push(self.item_from_row(&row)?);
305 }
306
307 if items.len() != indexes.len() {
310 return Err(SqliteDatasetError::Other(
311 "fewer rows than indexes requested; row_id values must be contiguous and \
312 start at 1",
313 ));
314 }
315
316 Ok(items)
317 })
318 }
319
320 fn len(&self) -> usize {
322 self.len
323 }
324}
325
326fn fetch_columns_and_len(
328 conn_pool: &Pool<TursoConnectionManager>,
329 select_statement: &str,
330 split: &str,
331) -> Result<(Vec<String>, usize)> {
332 let connection = conn_pool.get()?;
333
334 let (columns, max_row_id) = block_on(async {
335 let statement = connection.prepare(select_statement).await?;
337 let columns = statement.column_names();
338
339 let mut statement = connection
352 .prepare(format!("select max(row_id) from {split}").as_str())
353 .await?;
354 let max_row_id = statement.query_row(()).await?.get_value(0)?;
355
356 Ok::<_, turso::Error>((columns, max_row_id))
357 })?;
358
359 let len = match max_row_id {
363 Value::Null => 0,
364 Value::Integer(max_row_id) => usize::try_from(max_row_id).map_err(|_| {
365 SqliteDatasetError::Other("row_id is negative, so it cannot index the dataset")
366 })?,
367 _ => {
368 return Err(SqliteDatasetError::Other(
369 "row_id is not an integer, so it cannot index the dataset",
370 ));
371 }
372 };
373
374 Ok((columns, len))
375}
376
377#[derive(Debug)]
382struct TursoConnectionManager {
383 database: Database,
384}
385
386impl r2d2::ManageConnection for TursoConnectionManager {
387 type Connection = Connection;
388 type Error = turso::Error;
389
390 fn connect(&self) -> core::result::Result<Connection, turso::Error> {
391 self.database.connect()
392 }
393
394 fn is_valid(&self, _conn: &mut Connection) -> core::result::Result<(), turso::Error> {
395 Ok(())
396 }
397
398 fn has_broken(&self, _conn: &mut Connection) -> bool {
399 false
400 }
401}
402
403fn open_database<P: AsRef<Path>>(db_file: P, write: bool) -> Result<Database> {
406 let db_file = db_file.as_ref().to_str().ok_or(SqliteDatasetError::Other(
411 "database path is not valid UTF-8, which the turso engine requires",
412 ))?;
413
414 Ok(block_on(
417 Builder::new_local(db_file).read_only(!write).build(),
418 )?)
419}
420
421#[derive(Clone, Debug)]
424pub struct SqliteDatasetStorage {
425 name: Option<String>,
426 db_file: Option<PathBuf>,
427 base_dir: Option<PathBuf>,
428}
429
430impl SqliteDatasetStorage {
431 pub fn from_name(name: &str) -> Self {
437 SqliteDatasetStorage {
438 name: Some(name.to_string()),
439 db_file: None,
440 base_dir: None,
441 }
442 }
443
444 pub fn from_file<P: AsRef<Path>>(db_file: P) -> Self {
450 SqliteDatasetStorage {
451 name: None,
452 db_file: Some(db_file.as_ref().to_path_buf()),
453 base_dir: None,
454 }
455 }
456
457 pub fn with_base_dir<P: AsRef<Path>>(mut self, base_dir: P) -> Self {
463 self.base_dir = Some(base_dir.as_ref().to_path_buf());
464 self
465 }
466
467 pub fn exists(&self) -> bool {
473 self.db_file().exists()
474 }
475
476 pub fn db_file(&self) -> PathBuf {
482 match &self.db_file {
483 Some(db_file) => db_file.clone(),
484 None => {
485 let name = sanitize(self.name.as_ref().expect("Name is not set"));
486 Self::base_dir(self.base_dir.to_owned()).join(format!("{name}.db"))
487 }
488 }
489 }
490
491 pub fn base_dir(base_dir: Option<PathBuf>) -> PathBuf {
501 match base_dir {
502 Some(base_dir) => base_dir,
503 None => dirs::cache_dir()
504 .expect("Could not get cache directory")
505 .join("burn-dataset"),
506 }
507 }
508
509 pub fn writer<I>(&self, overwrite: bool) -> Result<SqliteDatasetWriter<I>>
519 where
520 I: Clone + Send + Sync + Serialize + DeserializeOwned,
521 {
522 SqliteDatasetWriter::new(self.db_file(), overwrite)
523 }
524
525 pub fn reader<I>(&self, split: &str) -> Result<SqliteDataset<I>>
535 where
536 I: Clone + Send + Sync + Serialize + DeserializeOwned,
537 {
538 if !self.exists() {
539 panic!("The database file does not exist");
540 }
541
542 SqliteDataset::from_db_file(self.db_file(), split)
543 }
544}
545
546#[derive(Debug)]
557pub struct SqliteDatasetWriter<I> {
558 db_file: PathBuf,
559 db_file_tmp: Option<Handle<Writable>>,
560 overwrite: bool,
561 state: Option<Mutex<WriteState>>,
562 is_completed: Arc<RwLock<bool>>,
563 phantom: PhantomData<I>,
564}
565
566const WRITE_BATCH_SIZE: usize = 256;
577
578#[derive(Debug)]
584struct WriteState {
585 connection: Connection,
586 pending: usize,
589 lost_a_batch: bool,
593 splits: HashSet<String>,
595}
596
597impl WriteState {
598 fn insert(&mut self, split: &str, item: Vec<u8>) -> Result<usize> {
601 block_on(async {
602 if self.pending == 0 {
603 self.connection.execute("BEGIN", ()).await?;
604 }
605 self.pending += 1;
606
607 let insert_statement = format!("insert into {split} (item) values (?)");
608 let mut statement = self.connection.prepare_cached(&insert_statement).await?;
609 statement.execute([item]).await
610 })?;
611
612 let index = (self.connection.last_insert_rowid() - 1) as usize;
614
615 if self.pending >= WRITE_BATCH_SIZE {
616 self.commit()?;
617 }
618
619 Ok(index)
620 }
621
622 fn commit(&mut self) -> Result<()> {
624 if self.pending > 0 {
625 let committed = block_on(self.connection.execute("COMMIT", ()));
626
627 if committed.is_err() {
628 let _ = block_on(self.connection.execute("ROLLBACK", ()));
633 self.lost_a_batch = true;
634 }
635
636 self.pending = 0;
637 committed?;
638 }
639 Ok(())
640 }
641
642 fn checkpoint(&self) -> Result<()> {
657 if self.lost_a_batch {
658 return Err(SqliteDatasetError::Other(
659 "A batch was rolled back earlier, so this dataset is missing rows and will not be \
660 published. Rebuild it from scratch",
661 ));
662 }
663
664 let mut failed = false;
665 block_on(
666 self.connection
667 .pragma_query("wal_checkpoint(TRUNCATE)", |row| {
668 failed |= !matches!(row.get_value(0), Ok(Value::Integer(0)));
669 Ok(())
670 }),
671 )?;
672
673 if failed {
674 return Err(SqliteDatasetError::Other(
675 "Could not checkpoint the write-ahead log, so the database was left unpublished \
676 rather than published incomplete. Turso does not report why; enable the \
677 `tracing` feature and log at debug level to see the underlying cause",
678 ));
679 }
680
681 Ok(())
682 }
683
684 fn create_table(&mut self, split: &str) -> Result<()> {
689 if self.splits.contains(split) {
690 return Ok(());
691 }
692
693 self.commit()?;
694 let create_table_statement = format!(
695 "create table if not exists {split} (row_id integer primary key autoincrement not \
696 null, item blob not null)"
697 );
698 block_on(self.connection.execute(&create_table_statement, ()))?;
699 self.splits.insert(split.to_string());
700 Ok(())
701 }
702}
703
704impl<I> SqliteDatasetWriter<I>
705where
706 I: Clone + Send + Sync + Serialize + DeserializeOwned,
707{
708 pub fn new<P: AsRef<Path>>(db_file: P, overwrite: bool) -> Result<Self> {
719 let writer = Self {
720 db_file: db_file.as_ref().to_path_buf(),
721 db_file_tmp: None,
722 overwrite,
723 state: None,
724 is_completed: Arc::new(RwLock::new(false)),
725 phantom: PhantomData,
726 };
727
728 writer.init()
729 }
730
731 fn init(mut self) -> Result<Self> {
737 if self.db_file.exists() {
739 if self.overwrite {
740 fs::remove_file(&self.db_file)?;
741 } else {
742 return Err(SqliteDatasetError::FileExists(self.db_file.clone()));
743 }
744 }
745
746 remove_wal_file(&self.db_file)?;
750
751 let db_file_dir = self
753 .db_file
754 .parent()
755 .ok_or("Unable to get parent directory")?;
756
757 if !db_file_dir.exists() {
758 fs::create_dir_all(db_file_dir)?;
759 }
760
761 let db_file_tmp = tmp_db_file(&self.db_file);
763 if db_file_tmp.exists() {
764 fs::remove_file(&db_file_tmp)?;
765 }
766
767 remove_wal_file(&db_file_tmp)?;
770
771 gix_tempfile::signal::setup(Default::default());
775 self.db_file_tmp = Some(gix_tempfile::writable_at(
776 &db_file_tmp,
777 ContainingDirectory::Exists,
778 AutoRemove::Tempfile,
779 )?);
780
781 let connection = open_database(&db_file_tmp, true)?.connect()?;
783 self.state = Some(Mutex::new(WriteState {
784 connection,
785 pending: 0,
786 lost_a_batch: false,
787 splits: HashSet::new(),
788 }));
789
790 Ok(self)
791 }
792
793 pub fn write(&self, split: &str, item: &I) -> Result<usize> {
811 let is_completed = self
813 .is_completed
814 .read()
815 .unwrap_or_else(PoisonError::into_inner);
816
817 if *is_completed {
819 return Err(SqliteDatasetError::Other(
820 "Cannot save to a completed dataset writer",
821 ));
822 }
823
824 let serialized_item = rmp_serde::to_vec(item)?;
827
828 let state = self.state.as_ref().ok_or(SqliteDatasetError::Other(
829 "Cannot save to a completed dataset writer",
830 ))?;
831 let mut state = state.lock().unwrap_or_else(PoisonError::into_inner);
832
833 state.create_table(split)?;
835 state.insert(split, serialized_item)
836 }
837
838 pub fn set_completed(&mut self) -> Result<()> {
840 let mut is_completed = self
841 .is_completed
842 .write()
843 .unwrap_or_else(PoisonError::into_inner);
844
845 {
853 let state = self
854 .state
855 .as_mut()
856 .ok_or(SqliteDatasetError::Other(
857 "Cannot complete a dataset writer twice",
858 ))?
859 .get_mut()
860 .unwrap_or_else(PoisonError::into_inner);
861 state.commit()?;
862 state.checkpoint()?;
863 }
864
865 self.state = None;
868
869 let _file_result = self
871 .db_file_tmp
872 .take() .unwrap() .persist(&self.db_file)?
875 .ok_or("Unable to persist the database file")?;
876
877 let _ = fs::remove_file(wal_file(&tmp_db_file(&self.db_file)));
881
882 *is_completed = true;
883 Ok(())
884 }
885}
886
887impl<I> Drop for SqliteDatasetWriter<I> {
888 fn drop(&mut self) {
899 self.state = None;
901 let _ = fs::remove_file(wal_file(&tmp_db_file(&self.db_file)));
902 }
903}
904
905fn tmp_db_file(db_file: &Path) -> PathBuf {
907 let mut db_file_tmp = db_file.to_path_buf();
908 db_file_tmp.set_extension("db.tmp");
909 db_file_tmp
910}
911
912fn wal_file(db_file: &Path) -> PathBuf {
914 let mut wal_file = OsString::from(db_file);
915 wal_file.push("-wal");
916 PathBuf::from(wal_file)
917}
918
919fn remove_wal_file(db_file: &Path) -> Result<()> {
921 let wal_file = wal_file(db_file);
922 if wal_file.exists() {
923 fs::remove_file(wal_file)?;
924 }
925 Ok(())
926}
927
928#[cfg(test)]
929mod tests {
930 use rayon::prelude::*;
931 use rstest::{fixture, rstest};
932 use serde::{Deserialize, Serialize};
933 use tempfile::{NamedTempFile, TempDir, tempdir};
934
935 use super::*;
936
937 type SqlDs = SqliteDataset<Sample>;
938
939 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
940 pub struct Sample {
941 column_str: String,
942 column_bytes: Vec<u8>,
943 column_int: i64,
944 column_bool: bool,
945 column_float: f64,
946 }
947
948 #[fixture]
949 fn train_dataset() -> SqlDs {
950 SqliteDataset::<Sample>::from_db_file("tests/data/sqlite-dataset.db", "train").unwrap()
951 }
952
953 #[rstest]
954 pub fn len(train_dataset: SqlDs) {
955 assert_eq!(train_dataset.len(), 2);
956 }
957
958 #[rstest]
959 pub fn get_some(train_dataset: SqlDs) {
960 let item = train_dataset.get(0).unwrap();
961 assert_eq!(item.column_str, "HI1");
962 assert_eq!(item.column_bytes, vec![55, 231, 159]);
963 assert_eq!(item.column_int, 1);
964 assert!(item.column_bool);
965 assert_eq!(item.column_float, 1.0);
966 }
967
968 #[rstest]
969 #[should_panic(expected = "Index out of bounds")]
970 pub fn get_none(train_dataset: SqlDs) {
971 train_dataset.get(10).unwrap();
972 }
973
974 #[rstest]
975 pub fn get_many_out_of_order_with_duplicates(train_dataset: SqlDs) {
976 let items = train_dataset.get_many(vec![1, 0, 1]).unwrap();
978
979 assert_eq!(items.len(), 3);
980 assert_eq!(items[0], train_dataset.get(1).unwrap());
981 assert_eq!(items[1], train_dataset.get(0).unwrap());
982 assert_eq!(items[2], train_dataset.get(1).unwrap());
983 }
984
985 #[rstest]
986 pub fn get_many_empty(train_dataset: SqlDs) {
987 assert_eq!(train_dataset.get_many(vec![]).unwrap(), Vec::new());
988 }
989
990 #[rstest]
991 #[should_panic(expected = "Index out of bounds")]
992 pub fn get_many_out_of_bounds(train_dataset: SqlDs) {
993 train_dataset.get_many(vec![0, 10]).unwrap();
994 }
995
996 #[rstest]
997 pub fn multi_thread(train_dataset: SqlDs) {
998 let dataset_len = train_dataset.len();
999 let indices: Vec<usize> = vec![0, 1, 1, 3, 4, 5, 6, 0, 8, 1];
1000 let valid_indices: Vec<usize> = indices.into_iter().filter(|&i| i < dataset_len).collect();
1001
1002 let results: Vec<Sample> = valid_indices
1003 .par_iter()
1004 .map(|&i| train_dataset.get(i).unwrap())
1005 .collect();
1006
1007 assert_eq!(results.len(), 5);
1008 }
1009
1010 #[rstest]
1016 pub fn reading_leaves_the_database_file_untouched(tmp_dir: TempDir) {
1017 let db_file = tmp_dir.path().join("sqlite-dataset.db");
1018 fs::copy("tests/data/sqlite-dataset.db", &db_file).unwrap();
1019 let before = fs::read(&db_file).unwrap();
1020
1021 let dataset = SqliteDataset::<Sample>::from_db_file(&db_file, "train").unwrap();
1022 assert_eq!(dataset.len(), 2);
1023 dataset.get(0).unwrap();
1024 dataset.get_many(vec![0, 1]).unwrap();
1025 drop(dataset);
1026
1027 assert_eq!(fs::read(&db_file).unwrap(), before);
1028 assert!(!wal_file(&db_file).exists());
1029 }
1030
1031 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1032 pub struct Typed {
1033 #[serde(rename = "MedInc")]
1034 median_income: f32,
1035 count: usize,
1036 ratio: f64,
1037 label: Option<String>,
1038 missing: Option<f64>,
1039 }
1040
1041 #[derive(Debug, Clone, Deserialize, PartialEq)]
1042 enum Status {
1043 Ready,
1044 Pending,
1045 }
1046
1047 #[derive(Debug, Clone, Deserialize, PartialEq)]
1048 struct UserId(i64);
1049
1050 #[derive(Debug, Clone, Deserialize, PartialEq)]
1051 struct Marker;
1052
1053 #[derive(Debug, Clone, Deserialize, PartialEq)]
1054 struct SerdeShapes {
1055 status: Status,
1056 user_id: UserId,
1057 marker: Marker,
1058 }
1059
1060 #[rstest]
1063 fn get_maps_columns_onto_serde_shapes(tmp_dir: TempDir) {
1064 let db_file = tmp_dir.path().join("serde-shapes.db");
1065
1066 {
1067 let connection = open_database(&db_file, true).unwrap().connect().unwrap();
1068 block_on(async {
1069 connection
1070 .execute(
1071 "create table train (status TEXT, user_id INTEGER, marker TEXT, \
1072 row_id INTEGER NOT NULL, PRIMARY KEY (row_id))",
1073 (),
1074 )
1075 .await?;
1076 connection
1077 .execute("insert into train values ('Ready', 42, 'Marker', 1)", ())
1078 .await?;
1079 connection
1080 .execute(
1081 "create table statuses (status TEXT, row_id INTEGER NOT NULL, \
1082 PRIMARY KEY (row_id))",
1083 (),
1084 )
1085 .await?;
1086 connection
1087 .execute("insert into statuses values ('Pending', 1)", ())
1088 .await?;
1089 connection
1090 .execute(
1091 "create table users (user_id INTEGER, row_id INTEGER NOT NULL, \
1092 PRIMARY KEY (row_id))",
1093 (),
1094 )
1095 .await?;
1096 connection
1097 .execute("insert into users values (7, 1)", ())
1098 .await?;
1099 connection
1100 .pragma_query("wal_checkpoint(TRUNCATE)", |_| Ok(()))
1101 .await
1102 })
1103 .unwrap();
1104 }
1105 fs::remove_file(wal_file(&db_file)).unwrap();
1106
1107 let dataset = SqliteDataset::<SerdeShapes>::from_db_file(&db_file, "train").unwrap();
1108 assert_eq!(
1109 dataset.get(0).unwrap(),
1110 SerdeShapes {
1111 status: Status::Ready,
1112 user_id: UserId(42),
1113 marker: Marker,
1114 }
1115 );
1116
1117 let statuses = SqliteDataset::<Status>::from_db_file(&db_file, "statuses").unwrap();
1118 assert_eq!(statuses.get(0).unwrap(), Status::Pending);
1119
1120 let users = SqliteDataset::<UserId>::from_db_file(&db_file, "users").unwrap();
1121 assert_eq!(users.get(0).unwrap(), UserId(7));
1122 }
1123
1124 #[rstest]
1128 pub fn get_maps_columns_onto_typed_fields(tmp_dir: TempDir) {
1129 let db_file = tmp_dir.path().join("typed.db");
1130
1131 {
1132 let connection = open_database(&db_file, true).unwrap().connect().unwrap();
1133 block_on(async {
1134 connection
1135 .execute(
1136 "create table train (\"MedInc\" REAL, count INTEGER, ratio REAL, \
1137 label TEXT, missing REAL, row_id INTEGER NOT NULL, PRIMARY KEY (row_id))",
1138 (),
1139 )
1140 .await?;
1141 connection
1144 .execute(
1145 "insert into train values (8.3252, 41, 2, 'first', NULL, 1)",
1146 (),
1147 )
1148 .await?;
1149 connection
1150 .execute(
1151 "insert into train values (8.3014, 21, 0.5, NULL, 1.5, 2)",
1152 (),
1153 )
1154 .await?;
1155 connection
1158 .execute(
1159 "insert into train values (NULL, 7, 1.0, 'third', NULL, 3)",
1160 (),
1161 )
1162 .await?;
1163 connection
1164 .pragma_query("wal_checkpoint(TRUNCATE)", |_| Ok(()))
1165 .await
1166 })
1167 .unwrap();
1168 }
1169 fs::remove_file(wal_file(&db_file)).unwrap();
1170
1171 let dataset = SqliteDataset::<Typed>::from_db_file(&db_file, "train").unwrap();
1172 assert_eq!(dataset.len(), 3);
1173 assert_eq!(
1174 dataset.get(0).unwrap(),
1175 Typed {
1176 median_income: 8.3252,
1177 count: 41,
1178 ratio: 2.0,
1179 label: Some("first".to_string()),
1180 missing: None,
1181 }
1182 );
1183 assert_eq!(
1184 dataset.get(1).unwrap(),
1185 Typed {
1186 median_income: 8.3014,
1187 count: 21,
1188 ratio: 0.5,
1189 label: None,
1190 missing: Some(1.5),
1191 }
1192 );
1193
1194 let third = dataset.get(2).unwrap();
1196 assert!(third.median_income.is_nan());
1197 assert_eq!(third.count, 7);
1198 assert_eq!(third.label, Some("third".to_string()));
1199 }
1200
1201 #[test]
1202 fn sqlite_dataset_storage() {
1203 let storage = SqliteDatasetStorage::from_file("non-existing.db");
1205 assert!(!storage.exists());
1206
1207 let storage = SqliteDatasetStorage::from_name("non-existing.db");
1209 assert!(!storage.exists());
1210
1211 let storage = SqliteDatasetStorage::from_file("tests/data/sqlite-dataset.db");
1213 assert!(storage.exists());
1214 let result = storage.reader::<Sample>("train");
1215 assert!(result.is_ok());
1216 let train = result.unwrap();
1217 assert_eq!(train.len(), 2);
1218
1219 let temp_file = NamedTempFile::new().unwrap();
1221 let storage = SqliteDatasetStorage::from_file(temp_file.path());
1222 assert!(storage.exists());
1223 let result = storage.writer::<Sample>(true);
1224 assert!(result.is_ok());
1225 }
1226
1227 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1228 pub struct Complex {
1229 column_str: String,
1230 column_bytes: Vec<u8>,
1231 column_int: i64,
1232 column_bool: bool,
1233 column_float: f64,
1234 column_complex: Vec<Vec<Vec<[u8; 3]>>>,
1235 }
1236
1237 #[fixture]
1239 fn tmp_dir() -> TempDir {
1240 tempdir().unwrap()
1243 }
1244 type Writer = SqliteDatasetWriter<Complex>;
1245
1246 #[fixture]
1249 fn writer_fixture(tmp_dir: TempDir) -> (Writer, TempDir) {
1250 let temp_dir_str = tmp_dir.path();
1251 let storage = SqliteDatasetStorage::from_name("preprocessed").with_base_dir(temp_dir_str);
1252 let overwrite = true;
1253 let result = storage.writer::<Complex>(overwrite);
1254 assert!(result.is_ok());
1255 let writer = result.unwrap();
1256 (writer, tmp_dir)
1257 }
1258
1259 #[test]
1260 fn test_new() {
1261 let test_path = NamedTempFile::new().unwrap();
1263 let _writer = SqliteDatasetWriter::<Complex>::new(&test_path, true).unwrap();
1264 assert!(!test_path.path().exists());
1265
1266 let test_path = NamedTempFile::new().unwrap();
1268 let result = SqliteDatasetWriter::<Complex>::new(&test_path, false);
1269 assert!(result.is_err());
1270
1271 let temp = NamedTempFile::new().unwrap();
1273 let test_path = temp.path().to_path_buf();
1274 assert!(temp.close().is_ok());
1275 assert!(!test_path.exists());
1276 let _writer = SqliteDatasetWriter::<Complex>::new(&test_path, true).unwrap();
1277 assert!(!test_path.exists());
1278 }
1279
1280 #[rstest]
1281 pub fn sqlite_writer_write(writer_fixture: (Writer, TempDir)) {
1282 let (writer, _tmp_dir) = writer_fixture;
1284
1285 assert!(writer.overwrite);
1286 assert!(!writer.db_file.exists());
1287
1288 let new_item = Complex {
1289 column_str: "HI1".to_string(),
1290 column_bytes: vec![1_u8, 2, 3],
1291 column_int: 0,
1292 column_bool: true,
1293 column_float: 1.0,
1294 column_complex: vec![vec![vec![[1, 23_u8, 3]]]],
1295 };
1296
1297 let index = writer.write("train", &new_item).unwrap();
1298 assert_eq!(index, 0);
1299
1300 let mut writer = writer;
1301
1302 writer.set_completed().expect("Failed to set completed");
1303
1304 assert!(writer.db_file.exists());
1305 assert!(writer.db_file_tmp.is_none());
1306
1307 let result = writer.write("train", &new_item);
1308
1309 assert!(result.is_err());
1311
1312 let dataset = SqliteDataset::<Complex>::from_db_file(&writer.db_file, "train").unwrap();
1313
1314 let fetched_item = dataset.get(0).unwrap();
1315 assert_eq!(fetched_item, new_item);
1316 assert_eq!(dataset.len(), 1);
1317 }
1318
1319 #[rstest]
1320 pub fn sqlite_writer_write_multi_thread(writer_fixture: (Writer, TempDir)) {
1321 let (writer, _tmp_dir) = writer_fixture;
1323
1324 let writer = Arc::new(writer);
1325 let record_count = 20;
1326
1327 let splits = ["train", "test"];
1328
1329 (0..record_count).into_par_iter().for_each(|index: i64| {
1330 let thread_id: std::thread::ThreadId = std::thread::current().id();
1331 let sample = Complex {
1332 column_str: format!("test_{thread_id:?}_{index}"),
1333 column_bytes: vec![index as u8, 2, 3],
1334 column_int: index,
1335 column_bool: true,
1336 column_float: 1.0,
1337 column_complex: vec![vec![vec![[1, index as u8, 3]]]],
1338 };
1339
1340 let split = splits[index as usize % 2];
1342
1343 let _index = writer.write(split, &sample).unwrap();
1344 });
1345
1346 let mut writer = Arc::try_unwrap(writer).unwrap();
1347
1348 writer
1349 .set_completed()
1350 .expect("Should set completed successfully");
1351
1352 let train =
1353 SqliteDataset::<Complex>::from_db_file(writer.db_file.clone(), "train").unwrap();
1354 let test = SqliteDataset::<Complex>::from_db_file(&writer.db_file, "test").unwrap();
1355
1356 assert_eq!(train.len(), record_count as usize / 2);
1357 assert_eq!(test.len(), record_count as usize / 2);
1358 }
1359
1360 #[rstest]
1365 pub fn a_lost_batch_can_never_be_published(writer_fixture: (Writer, TempDir)) {
1366 let (writer, _tmp_dir) = writer_fixture;
1367 let item = Complex {
1368 column_str: "HI".to_string(),
1369 column_bytes: vec![1, 2, 3],
1370 column_int: 0,
1371 column_bool: true,
1372 column_float: 1.0,
1373 column_complex: vec![vec![vec![[1, 2, 3]]]],
1374 };
1375 writer.write("train", &item).unwrap();
1376
1377 writer.state.as_ref().unwrap().lock().unwrap().lost_a_batch = true;
1379
1380 let mut writer = writer;
1381 assert!(writer.set_completed().is_err(), "published a lost batch");
1382 assert!(!writer.db_file.exists());
1383
1384 writer.write("train", &item).unwrap();
1386 assert!(writer.set_completed().is_err(), "published a lost batch");
1387 assert!(!writer.db_file.exists());
1388 }
1389
1390 #[rstest]
1397 pub fn get_many_handles_batches_past_the_sqlite_variable_limit(tmp_dir: TempDir) {
1398 let db_file = tmp_dir.path().join("wide.db");
1399 let item = |index: usize| Complex {
1400 column_str: format!("item_{index}"),
1401 column_bytes: vec![index as u8, 2, 3],
1402 column_int: index as i64,
1403 column_bool: true,
1404 column_float: 1.0,
1405 column_complex: vec![vec![vec![[1, index as u8, 3]]]],
1406 };
1407
1408 let count = 1200;
1410 let mut writer = SqliteDatasetWriter::<Complex>::new(&db_file, true).unwrap();
1411 for index in 0..count {
1412 writer.write("train", &item(index)).unwrap();
1413 }
1414 writer.set_completed().unwrap();
1415
1416 let dataset = SqliteDataset::<Complex>::from_db_file(&db_file, "train").unwrap();
1417 let items = dataset.get_many((0..count).collect()).unwrap();
1418 assert_eq!(items.len(), count);
1419 for (index, got) in items.iter().enumerate() {
1420 assert_eq!(got.column_int, index as i64);
1421 }
1422 }
1423
1424 #[rstest]
1429 pub fn non_integer_row_id_is_an_error(tmp_dir: TempDir) {
1430 let db_file = tmp_dir.path().join("textual.db");
1431
1432 {
1433 let connection = open_database(&db_file, true).unwrap().connect().unwrap();
1434 block_on(async {
1435 connection
1436 .execute("create table train (row_id TEXT, name TEXT)", ())
1437 .await?;
1438 connection
1439 .execute("insert into train values ('a', 'first')", ())
1440 .await?;
1441 connection
1442 .pragma_query("wal_checkpoint(TRUNCATE)", |_| Ok(()))
1443 .await
1444 })
1445 .unwrap();
1446 }
1447 fs::remove_file(wal_file(&db_file)).unwrap();
1448
1449 #[derive(Debug, Clone, Serialize, Deserialize)]
1450 struct Named {
1451 name: String,
1452 }
1453
1454 let result = SqliteDataset::<Named>::from_db_file(&db_file, "train");
1455 assert!(
1456 result.is_err(),
1457 "a textual row_id should be rejected, not reported as an empty dataset"
1458 );
1459 }
1460
1461 #[rstest]
1466 pub fn sqlite_writer_abandoned_leaves_nothing_behind(tmp_dir: TempDir) {
1467 let db_file = tmp_dir.path().join("abandoned.db");
1468 let item = Complex {
1469 column_str: "HI".to_string(),
1470 column_bytes: vec![1, 2, 3],
1471 column_int: 0,
1472 column_bool: true,
1473 column_float: 1.0,
1474 column_complex: vec![vec![vec![[1, 2, 3]]]],
1475 };
1476
1477 {
1478 let writer = SqliteDatasetWriter::<Complex>::new(&db_file, true).unwrap();
1479 for _ in 0..1000 {
1480 writer.write("train", &item).unwrap();
1481 }
1482 }
1484
1485 let db_file_tmp = tmp_db_file(&db_file);
1486 assert!(!db_file_tmp.exists(), "temporary database left behind");
1487 assert!(
1488 !wal_file(&db_file_tmp).exists(),
1489 "abandoned writer leaked its write-ahead log"
1490 );
1491 assert!(!db_file.exists(), "nothing should have been published");
1492 }
1493
1494 #[rstest]
1498 pub fn concurrent_reads_scale_across_the_pool(tmp_dir: TempDir) {
1499 let db_file = tmp_dir.path().join("concurrent.db");
1500 let item = |index: usize| Complex {
1501 column_str: format!("item_{index}"),
1502 column_bytes: vec![index as u8, 2, 3],
1503 column_int: index as i64,
1504 column_bool: true,
1505 column_float: 1.0,
1506 column_complex: vec![vec![vec![[1, index as u8, 3]]]],
1507 };
1508
1509 let mut writer = SqliteDatasetWriter::<Complex>::new(&db_file, true).unwrap();
1510 for index in 0..1000 {
1511 writer.write("train", &item(index)).unwrap();
1512 }
1513 writer.set_completed().unwrap();
1514
1515 let dataset = SqliteDataset::<Complex>::from_db_file(&db_file, "train").unwrap();
1516
1517 let requested: Vec<usize> = (0..8000).map(|i| i % 1000).collect();
1518 let got: Vec<i64> = requested
1519 .par_iter()
1520 .map(|&index| dataset.get(index).unwrap().column_int)
1521 .collect();
1522 for (position, &index) in requested.iter().enumerate() {
1523 assert_eq!(got[position], index as i64);
1524 }
1525
1526 (0..256usize).into_par_iter().for_each(|batch| {
1528 let indexes: Vec<usize> = (0..32).map(|offset| (batch * 32 + offset) % 1000).collect();
1529 let items = dataset.get_many(indexes.clone()).unwrap();
1530 assert_eq!(items.len(), indexes.len());
1531 for (item, index) in items.iter().zip(&indexes) {
1532 assert_eq!(item.column_int, *index as i64);
1533 }
1534 });
1535 }
1536
1537 #[rstest]
1543 pub fn sqlite_writer_set_completed_is_retryable(writer_fixture: (Writer, TempDir)) {
1544 let (writer, _tmp_dir) = writer_fixture;
1545 let item = Complex {
1546 column_str: "HI".to_string(),
1547 column_bytes: vec![1, 2, 3],
1548 column_int: 0,
1549 column_bool: true,
1550 column_float: 1.0,
1551 column_complex: vec![vec![vec![[1, 2, 3]]]],
1552 };
1553 for _ in 0..500 {
1554 writer.write("train", &item).unwrap();
1555 }
1556
1557 let mut writer = writer;
1558
1559 let blocker = open_database(tmp_db_file(&writer.db_file), true)
1560 .unwrap()
1561 .connect()
1562 .unwrap();
1563 block_on(async {
1564 blocker.execute("BEGIN", ()).await?;
1565 blocker
1566 .prepare("select count(*) from train")
1567 .await?
1568 .query_row(())
1569 .await
1570 })
1571 .unwrap();
1572
1573 let first = writer.set_completed();
1574 assert!(first.is_err(), "checkpoint should have been refused");
1575 assert!(!writer.db_file.exists(), "nothing may be published yet");
1576
1577 assert!(
1579 writer.set_completed().is_err(),
1580 "retry published while the checkpoint was still refused"
1581 );
1582 assert!(!writer.db_file.exists(), "nothing may be published yet");
1583
1584 writer.write("train", &item).unwrap();
1586
1587 drop(blocker);
1589 writer
1590 .set_completed()
1591 .expect("should complete once the database is no longer busy");
1592
1593 let dataset = SqliteDataset::<Complex>::from_db_file(&writer.db_file, "train").unwrap();
1594 assert_eq!(dataset.len(), 501);
1595 assert!(writer.write("train", &item).is_err());
1596 }
1597
1598 #[rstest]
1604 pub fn sqlite_writer_write_across_batches(writer_fixture: (Writer, TempDir)) {
1605 let (writer, _tmp_dir) = writer_fixture;
1606 let record_count = WRITE_BATCH_SIZE * 2 + 5;
1607
1608 let item = |index: usize| Complex {
1609 column_str: format!("item_{index}"),
1610 column_bytes: vec![index as u8, 2, 3],
1611 column_int: index as i64,
1612 column_bool: true,
1613 column_float: 1.0,
1614 column_complex: vec![vec![vec![[1, index as u8, 3]]]],
1615 };
1616
1617 for index in 0..record_count {
1618 assert_eq!(writer.write("train", &item(index)).unwrap(), index);
1619 }
1620
1621 let mut writer = writer;
1622 writer.set_completed().expect("Failed to set completed");
1623
1624 let train = SqliteDataset::<Complex>::from_db_file(&writer.db_file, "train").unwrap();
1625 assert_eq!(train.len(), record_count);
1626 assert_eq!(train.get(0).unwrap(), item(0));
1627 assert_eq!(train.get(WRITE_BATCH_SIZE).unwrap(), item(WRITE_BATCH_SIZE));
1628 assert_eq!(train.get(record_count - 1).unwrap(), item(record_count - 1));
1629
1630 assert!(!wal_file(&tmp_db_file(&writer.db_file)).exists());
1632 }
1633}