From b8de369e3344d0947e9b8cff470fd376625b3dd3 Mon Sep 17 00:00:00 2001 From: Louis Dureuil Date: Wed, 30 Nov 2022 14:54:34 +0100 Subject: [PATCH] Add v1 reader --- dump/src/reader/v1/mod.rs | 318 +++++++++++------- ...mp__reader__v1__test__read_dump_v1-10.snap | 24 ++ ...mp__reader__v1__test__read_dump_v1-12.snap | 24 ++ ...ump__reader__v1__test__read_dump_v1-2.snap | 38 +++ ...ump__reader__v1__test__read_dump_v1-5.snap | 28 ++ ...ump__reader__v1__test__read_dump_v1-6.snap | 28 ++ ...ump__reader__v1__test__read_dump_v1-7.snap | 28 ++ ...ump__reader__v1__test__read_dump_v1-8.snap | 24 ++ dump/src/reader/v1/update.rs | 46 --- dump/src/reader/v1/v1.rs | 22 -- dump/tests/assets/v1.dump | Bin 0 -> 9904 bytes 11 files changed, 398 insertions(+), 182 deletions(-) create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-10.snap create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-12.snap create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-2.snap create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-5.snap create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-6.snap create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-7.snap create mode 100644 dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-8.snap delete mode 100644 dump/src/reader/v1/v1.rs create mode 100644 dump/tests/assets/v1.dump diff --git a/dump/src/reader/v1/mod.rs b/dump/src/reader/v1/mod.rs index f638262cc..1932b602a 100644 --- a/dump/src/reader/v1/mod.rs +++ b/dump/src/reader/v1/mod.rs @@ -1,173 +1,263 @@ use std::{ - convert::Infallible, fs::{self, File}, io::{BufRead, BufReader}, - path::Path, + path::{Path, PathBuf}, }; use tempfile::TempDir; use time::OffsetDateTime; -use self::update::UpdateStatus; - -use super::{DumpReader, IndexReader}; -use crate::{Error, Result, Version}; +use super::{compat::v1_to_v2::CompatV1ToV2, Document}; +use crate::{IndexMetadata, Result, Version}; +use serde::Deserialize; pub mod settings; pub mod update; -pub mod v1; pub struct V1Reader { - dump: TempDir, - metadata: v1::Metadata, - indexes: Vec, + pub dump: TempDir, + pub db_version: String, + pub dump_version: crate::Version, + indexes: Vec, } -struct V1IndexReader { - name: String, +pub struct IndexUuid { + pub name: String, + pub uid: String, +} +pub type Task = self::update::UpdateStatus; + +struct V1Index { + metadata: IndexMetadataV1, + path: PathBuf, +} + +impl V1Index { + pub fn new(path: PathBuf, metadata: Index) -> Self { + Self { metadata: metadata.into(), path } + } + + pub fn open(&self) -> Result { + V1IndexReader::new(&self.path, self.metadata.clone()) + } + + pub fn metadata(&self) -> &IndexMetadata { + &self.metadata.metadata + } +} + +pub struct V1IndexReader { + metadata: IndexMetadataV1, documents: BufReader, settings: BufReader, updates: BufReader, - - current_update: Option, } impl V1IndexReader { - pub fn new(name: String, path: &Path) -> Result { - let mut ret = V1IndexReader { - name, + pub fn new(path: &Path, metadata: IndexMetadataV1) -> Result { + Ok(V1IndexReader { + metadata, documents: BufReader::new(File::open(path.join("documents.jsonl"))?), settings: BufReader::new(File::open(path.join("settings.json"))?), updates: BufReader::new(File::open(path.join("updates.jsonl"))?), - current_update: None, - }; - ret.next_update(); - - Ok(ret) + }) } - pub fn next_update(&mut self) -> Result> { - let current_update = if let Some(line) = self.updates.lines().next() { - Some(serde_json::from_str(&line?)?) - } else { - None - }; + pub fn metadata(&self) -> &IndexMetadata { + &self.metadata.metadata + } - Ok(std::mem::replace(&mut self.current_update, current_update)) + pub fn documents(&mut self) -> Result> + '_> { + Ok((&mut self.documents) + .lines() + .map(|line| -> Result<_> { Ok(serde_json::from_str(&line?)?) })) + } + + pub fn settings(&mut self) -> Result { + Ok(serde_json::from_reader(&mut self.settings)?) + } + + pub fn tasks(self) -> impl Iterator> { + self.updates.lines().map(|line| -> Result<_> { Ok(serde_json::from_str(&line?)?) }) } } impl V1Reader { pub fn open(dump: TempDir) -> Result { - let mut meta_file = fs::read(dump.path().join("metadata.json"))?; - let metadata = serde_json::from_reader(&*meta_file)?; + let meta_file = fs::read(dump.path().join("metadata.json"))?; + let metadata: Metadata = serde_json::from_reader(&*meta_file)?; let mut indexes = Vec::new(); - let entries = fs::read_dir(dump.path())?; - for entry in entries { - let entry = entry?; - if entry.file_type()?.is_dir() { - indexes.push(V1IndexReader::new( - entry - .file_name() - .to_str() - .ok_or(Error::BadIndexName)? - .to_string(), - &entry.path(), - )?); - } + for index in metadata.indexes.into_iter() { + let index_path = dump.path().join(&index.uid); + indexes.push(V1Index::new(index_path, index)); } Ok(V1Reader { dump, - metadata, indexes, + db_version: metadata.db_version, + dump_version: metadata.dump_version, }) } - fn next_update(&mut self) -> Result> { - if let Some((idx, _)) = self - .indexes + pub fn to_v2(self) -> CompatV1ToV2 { + CompatV1ToV2 { from: self } + } + + pub fn index_uuid(&self) -> Vec { + self.indexes .iter() - .map(|index| index.current_update) - .enumerate() - .filter_map(|(idx, update)| update.map(|u| (idx, u))) - .min_by_key(|(_, update)| update.enqueued_at()) - { - self.indexes[idx].next_update() - } else { - Ok(None) + .map(|index| IndexUuid { + name: index.metadata.name.to_owned(), + uid: index.metadata().uid.to_owned(), + }) + .collect() + } + + pub fn version(&self) -> Version { + Version::V1 + } + + pub fn date(&self) -> Option { + None + } + + pub fn indexes(&self) -> Result> + '_> { + Ok(self.indexes.iter().map(|index| index.open())) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Index { + pub name: String, + pub uid: String, + #[serde(with = "time::serde::rfc3339")] + created_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + updated_at: OffsetDateTime, + pub primary_key: Option, +} + +#[derive(Clone)] +pub struct IndexMetadataV1 { + pub name: String, + pub metadata: crate::IndexMetadata, +} + +impl From for IndexMetadataV1 { + fn from(index: Index) -> Self { + IndexMetadataV1 { + name: index.name, + metadata: crate::IndexMetadata { + uid: index.uid, + primary_key: index.primary_key, + created_at: index.created_at, + updated_at: index.updated_at, + }, } } } -impl IndexReader for &V1IndexReader { - type Document = serde_json::Map; - type Settings = settings::Settings; - - fn name(&self) -> &str { - todo!() - } - - fn documents(&self) -> Result>>> { - todo!() - } - - fn settings(&self) -> Result { - todo!() - } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Metadata { + pub indexes: Vec, + pub db_version: String, + pub dump_version: crate::Version, } -impl DumpReader for V1Reader { - type Document = serde_json::Map; - type Settings = settings::Settings; +#[cfg(test)] +pub(crate) mod test { + use std::fs::File; + use std::io::BufReader; - type Task = update::UpdateStatus; - type UpdateFile = Infallible; + use flate2::bufread::GzDecoder; + use meili_snap::insta; + use tempfile::TempDir; - type Key = Infallible; + use super::*; - fn date(&self) -> Option { - None - } + #[test] + fn read_dump_v1() { + let dump = File::open("tests/assets/v1.dump").unwrap(); + let dir = TempDir::new().unwrap(); + let mut dump = BufReader::new(dump); + let gz = GzDecoder::new(&mut dump); + let mut archive = tar::Archive::new(gz); + archive.unpack(dir.path()).unwrap(); - fn version(&self) -> Version { - Version::V1 - } + let dump = V1Reader::open(dir).unwrap(); - fn indexes( - &self, - ) -> Result< - Box< - dyn Iterator< - Item = Result< - Box< - dyn super::IndexReader< - Document = Self::Document, - Settings = Self::Settings, - >, - >, - >, - >, - >, - > { - Ok(Box::new(self.indexes.iter().map(|index| { - let index = Box::new(index) - as Box>; - Ok(index) - }))) - } + // top level infos + assert_eq!(dump.date(), None); - fn tasks(&self) -> Box)>>> { - Box::new(std::iter::from_fn(|| { - self.next_update() - .transpose() - .map(|result| result.map(|task| (task, None))) - })) - } + // indexes + let mut indexes = dump.indexes().unwrap().collect::>>().unwrap(); - fn keys(&self) -> Box>> { - Box::new(std::iter::empty()) + let mut products = indexes.pop().unwrap(); + let mut movies = indexes.pop().unwrap(); + let mut dnd_spells = indexes.pop().unwrap(); + + assert!(indexes.is_empty()); + + // products + insta::assert_json_snapshot!(products.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "products", + "primaryKey": "sku", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(products.settings().unwrap()); + let documents = products.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"b01c8371aea4c7171af0d4d846a2bdca"); + + // products tasks + let tasks = products.tasks().collect::>>().unwrap(); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"91de507f206ad21964584021932ba7a7"); + + // movies + insta::assert_json_snapshot!(movies.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "movies", + "primaryKey": "id", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(movies.settings().unwrap()); + let documents = movies.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"b63dbed5bbc059f3e32bc471ae699bf5"); + + // movies tasks + let tasks = movies.tasks().collect::>>().unwrap(); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"55eef4de2bef7e84c5ce0bee47488f56"); + + // spells + insta::assert_json_snapshot!(dnd_spells.metadata(), { ".createdAt" => "[now]", ".updatedAt" => "[now]" }, @r###" + { + "uid": "dnd_spells", + "primaryKey": "index", + "createdAt": "[now]", + "updatedAt": "[now]" + } + "###); + + insta::assert_json_snapshot!(dnd_spells.settings().unwrap()); + let documents = dnd_spells.documents().unwrap().collect::>>().unwrap(); + assert_eq!(documents.len(), 10); + meili_snap::snapshot_hash!(format!("{:#?}", documents), @"aa24c0cfc733d66c396237ad44263bed"); + + // spells tasks + let tasks = dnd_spells.tasks().collect::>>().unwrap(); + meili_snap::snapshot_hash!(meili_snap::json_string!(tasks), @"836dd7d64d5ad20ad901c44b1b161a4c"); } } diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-10.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-10.snap new file mode 100644 index 000000000..f71df0ae6 --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-10.snap @@ -0,0 +1,24 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: dnd_spells.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": {}, + "attributesForFaceting": [] +} diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-12.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-12.snap new file mode 100644 index 000000000..f71df0ae6 --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-12.snap @@ -0,0 +1,24 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: dnd_spells.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": {}, + "attributesForFaceting": [] +} diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-2.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-2.snap new file mode 100644 index 000000000..b117c5f3d --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-2.snap @@ -0,0 +1,38 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: products.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": { + "android": [ + "phone", + "smartphone" + ], + "iphone": [ + "phone", + "smartphone" + ], + "phone": [ + "android", + "iphone", + "smartphone" + ] + }, + "attributesForFaceting": [] +} diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-5.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-5.snap new file mode 100644 index 000000000..aa9ed082a --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-5.snap @@ -0,0 +1,28 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: movies.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness", + "asc(release_date)" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": {}, + "attributesForFaceting": [ + "id", + "genres" + ] +} diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-6.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-6.snap new file mode 100644 index 000000000..aa9ed082a --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-6.snap @@ -0,0 +1,28 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: movies.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness", + "asc(release_date)" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": {}, + "attributesForFaceting": [ + "id", + "genres" + ] +} diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-7.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-7.snap new file mode 100644 index 000000000..aa9ed082a --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-7.snap @@ -0,0 +1,28 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: movies.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness", + "asc(release_date)" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": {}, + "attributesForFaceting": [ + "id", + "genres" + ] +} diff --git a/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-8.snap b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-8.snap new file mode 100644 index 000000000..f71df0ae6 --- /dev/null +++ b/dump/src/reader/v1/snapshots/dump__reader__v1__test__read_dump_v1-8.snap @@ -0,0 +1,24 @@ +--- +source: dump/src/reader/v1/mod.rs +expression: dnd_spells.settings().unwrap() +--- +{ + "rankingRules": [ + "typo", + "words", + "proximity", + "attribute", + "wordsPosition", + "exactness" + ], + "distinctAttribute": null, + "searchableAttributes": [ + "*" + ], + "displayedAttributes": [ + "*" + ], + "stopWords": [], + "synonyms": {}, + "attributesForFaceting": [] +} diff --git a/dump/src/reader/v1/update.rs b/dump/src/reader/v1/update.rs index c9ccaf309..b6408f42a 100644 --- a/dump/src/reader/v1/update.rs +++ b/dump/src/reader/v1/update.rs @@ -1,54 +1,8 @@ use serde::{Deserialize, Serialize}; -use serde_json::Value; use time::OffsetDateTime; use super::settings::SettingsUpdate; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Update { - data: UpdateData, - #[serde(with = "time::serde::rfc3339")] - enqueued_at: OffsetDateTime, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum UpdateData { - ClearAll, - Customs(Vec), - // (primary key, documents) - DocumentsAddition { - primary_key: Option, - documents: Vec>, - }, - DocumentsPartial { - primary_key: Option, - documents: Vec>, - }, - DocumentsDeletion(Vec), - Settings(Box), -} - -impl UpdateData { - pub fn update_type(&self) -> UpdateType { - match self { - UpdateData::ClearAll => UpdateType::ClearAll, - UpdateData::Customs(_) => UpdateType::Customs, - UpdateData::DocumentsAddition { documents, .. } => UpdateType::DocumentsAddition { - number: documents.len(), - }, - UpdateData::DocumentsPartial { documents, .. } => UpdateType::DocumentsPartial { - number: documents.len(), - }, - UpdateData::DocumentsDeletion(deletion) => UpdateType::DocumentsDeletion { - number: deletion.len(), - }, - UpdateData::Settings(update) => UpdateType::Settings { - settings: update.clone(), - }, - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "name")] pub enum UpdateType { diff --git a/dump/src/reader/v1/v1.rs b/dump/src/reader/v1/v1.rs deleted file mode 100644 index 0f4312508..000000000 --- a/dump/src/reader/v1/v1.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde::Deserialize; -use time::OffsetDateTime; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Index { - pub name: String, - pub uid: String, - #[serde(with = "time::serde::rfc3339")] - created_at: OffsetDateTime, - #[serde(with = "time::serde::rfc3339")] - updated_at: OffsetDateTime, - pub primary_key: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Metadata { - indexes: Vec, - db_version: String, - dump_version: crate::Version, -} diff --git a/dump/tests/assets/v1.dump b/dump/tests/assets/v1.dump new file mode 100644 index 0000000000000000000000000000000000000000..f1e2959362c60fe8a6fb4ac561fe65037ade8034 GIT binary patch literal 9904 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 literal 0 HcmV?d00001