summaryrefslogtreecommitdiff
path: root/src/database.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/database.rs')
-rw-r--r--src/database.rs687
1 files changed, 422 insertions, 265 deletions
diff --git a/src/database.rs b/src/database.rs
index 14ff853..bd5ae65 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -16,93 +16,96 @@
// License along with бирка-тян. If not, see <http://www.gnu.org/licenses/>.
use anyhow::Result;
-use blake2::{Blake2b, Digest};
use rusqlite::Connection;
use std::convert::AsRef;
-use std::io;
use std::path::Path;
-/// A connection to the tag database.
-#[derive(Debug)]
-pub struct TagDatabase(Connection);
-
-/// Tag query with a sense of "state" for pagination.
-#[derive(Debug)]
-pub struct Query<'a> {
- tags: &'a [&'a str],
- last_id: Option<i64>,
- limit: Option<i64>,
+/// Type of constraint on a column.
+#[derive(Debug, PartialEq)]
+pub enum Atom {
+ Is(String),
+ IsNot(String),
}
-impl<'a> Query<'a> {
- /// Construct a new query for images tagged with `tags`.
- ///
- /// When given to the database, this query will return at most `limit`
- /// results, if specified, and will return only images newer than `last_id`,
- /// if specified.
- pub fn new(tags: &'a [&'a str], last_id: Option<i64>, limit: Option<i64>) -> Self {
- Query {
- tags,
- last_id,
- limit,
- }
- }
+/// One of the addressable columns of the 'files' table.
+#[derive(Debug, PartialEq)]
+pub enum Field {
+ Tag(Atom),
+ Filename(Atom),
}
-/// The result of an image query in бирка-тян's tag database.
-#[derive(Debug)]
-pub struct Image {
- pub id: i64,
- pub blake2: String,
- pub filename: String,
- pub orig_dir: String,
-}
-
-/// Return a base64-encoded BLAKE2b hash identifying the file at `path`.
-pub fn hash_file(path: &Path) -> Result<String> {
- let mut file = std::fs::File::open(path)?;
- let mut hasher = Blake2b::new();
- io::copy(&mut file, &mut hasher)?;
- Ok(base64::encode(&hasher.result()))
-}
+use Atom::{Is, IsNot};
+use Field::{Filename, Tag};
-/// Split the path into a tuple containing the parent directory and the filename.
-pub fn split_path(path: &Path) -> Result<(String, String)> {
- if path.is_dir() {
- bail!("`path` does not name a file.");
- }
+/// A logical conjunction of `Field` contraints.
+///
+/// This is the general interface for queries to the tag database.
+pub type Query = Vec<Field>;
- let file_name = path
- .file_name()
- .and_then(|os| os.to_str())
- .ok_or(anyhow!("Couldn't parse file name."))?;
+/// Parse `query_string` into a `Query` object usable with the tag database.
+pub fn parse_query<T: AsRef<str>>(query_string: T) -> Result<Query> {
+ Ok(query_string
+ .as_ref()
+ .split_whitespace()
+ .map(|part| {
+ if part.starts_with("-") {
+ if part.len() >= 9 && &part[1..10] == "filename:" {
+ Filename(IsNot(String::from(&part[10..])))
+ } else {
+ Tag(IsNot(String::from(&part[1..])))
+ }
+ } else {
+ if part.len() >= 9 && &part[..9] == "filename:" {
+ Filename(Is(String::from(&part[9..])))
+ } else {
+ Tag(Is(String::from(part)))
+ }
+ }
+ })
+ .collect())
+}
- // Convert `path` to a string and strip off the `file_name` we've just
- // obtained to yield the name of the parent directory.
- let path = path.to_str().ok_or(anyhow!("Couldn't parse path."))?;
- let split_index = path.len() - file_name.len();
- let parent_directory = &path[0..split_index];
+/// State object representing a point in a paginated query.
+pub struct Keyset {
+ pub last_id: i64,
+ pub how_many: i64,
+}
- Ok((file_name.to_string(), parent_directory.to_string()))
+/// Information about an file in the database.
+///
+/// The result of any queries made to the tag database. `id` is the canonical
+/// representation of the file, `path` is the path of the file in the
+/// filesystem, and `tags` is a vector containing all of the strings the file is
+/// tagged with.
+pub struct File {
+ pub id: i64,
+ pub path: String,
+ pub tags: Vec<String>,
}
+/// Public interface to the tag database.
+///
+/// Essentially, a wrapper around `rusqlite::Connection` that allows only
+/// queries which are sensical given the purpose of the tag databse.
+pub struct TagDatabase(Connection);
+
impl TagDatabase {
- /// Instantiate a new `TagDatabase` whose backing database is at `path`.
- pub fn new<T: AsRef<Path>>(path: &T) -> Result<Self> {
+ /// Return a `TagDatabase` connected to `path`.
+ ///
+ /// This constructor will create the file if it does not exist.
+ pub fn new<T: AsRef<Path>>(path: T) -> Result<TagDatabase> {
let tb = TagDatabase(Connection::open(path)?);
tb.initialize_tables()?;
Ok(tb)
}
- /// Create the table structure for the tag database.
+ /// Initialize the tag database's schema.
fn initialize_tables(&self) -> Result<()> {
self.0.execute(
- "CREATE TABLE IF NOT EXISTS images (
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
- blake2 TEXT NOT NULL,
- filename TEXT NOT NULL,
- orig_dir TEXT NOT NULL
+ "CREATE TABLE IF NOT EXISTS files (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ path TEXT NOT NULL
)",
params![],
)?;
@@ -115,196 +118,235 @@ impl TagDatabase {
)?;
self.0.execute(
"CREATE TABLE IF NOT EXISTS mapping (
- image INTEGER NOT NULL,
- tag INTEGER NOT NULL
+ file INTEGER NOT NULL,
+ tag INTEGER NOT NULL
)",
params![],
)?;
Ok(())
}
- /// Insert `tag` into the database, if it does not already exist.
- pub fn add_tag(&self, tag: &str) -> Result<()> {
- let exists = self
+ /// Return all tags associated with the file named by `id`.
+ fn tags_for_file(&self, id: i64) -> Result<Vec<String>> {
+ Ok(self
.0
- .prepare("SELECT * FROM tags WHERE name = ?")?
- .exists(params![tag])?;
- if !exists {
- self.0
- .execute("INSERT INTO tags (name) VALUES(?)", params![tag])?;
- }
- Ok(())
+ .prepare(
+ "SELECT tags.name
+ FROM mapping
+ INNER JOIN tags ON mapping.tag = tags.id
+ WHERE mapping.file = ?;",
+ )?
+ .query_map(params![id], |row| row.get(0))?
+ .filter_map(|tag| tag.ok())
+ .collect())
}
- /// Return the internal id for `tag`.
- pub fn tag_id(&self, tag: &str) -> Result<i64> {
+ /// Return the id of the file located at `path`.
+ fn id_for_path<T: AsRef<Path>>(&self, path: T) -> Result<i64> {
+ let path = String::from(path.as_ref().to_str().unwrap());
Ok(self
.0
- .prepare("SELECT * FROM tags WHERE name = ?")?
- .query_row(params![tag], |row| Ok(row.get::<_, i64>(0)))??)
+ .prepare("SELECT id FROM files WHERE path = ?")?
+ .query_row(params![path], |row| Ok(row.get::<_, i64>(0)))??)
}
- /// Insert the image at `path` into the data.
- pub fn import_image(&self, path: &Path, hash: &str) -> Result<i64> {
- let (file_name, parent_directory) = split_path(path)?;
- Ok(self
+ /// Index the file at `path` in the tag database.
+ pub fn add_file<T, S>(&self, path: T, tags: &Vec<S>) -> Result<File>
+ where
+ T: AsRef<Path>,
+ S: AsRef<str>,
+ {
+ if self.id_for_path(&path).is_ok() {
+ bail!("'{:?}' already indexed by the tag database.", path.as_ref())
+ }
+
+ // Insert the file row.
+ self.0
+ .prepare("INSERT INTO files (path) VALUES(?)")?
+ .insert(params![path.as_ref().to_str().unwrap()])?;
+
+ // Associate each tag with the file, creating a row for the tag if it
+ // does not yet exist in the database.
+
+ let path = String::from(path.as_ref().to_str().unwrap());
+ let id = self
.0
- .prepare("INSERT INTO images (blake2, filename, orig_dir) VALUES(?,?,?)")?
- .insert(params![hash, file_name, parent_directory])?)
+ .prepare("SELECT id FROM files WHERE path = ?")?
+ .query_row(params![path], |row| Ok(row.get::<_, i64>(0)))??;
+ self.tag_file(id, tags)?;
+
+ let tags = self.tags_for_file(id)?;
+ Ok(File { id, path, tags })
}
- /// Return the internal id for the image at `path`.
- pub fn image_id(&self, path: &Path) -> Result<i64> {
- let (file_name, path) = split_path(path)?;
+ /// Remove the file named by `id` from the tag database.
+ pub fn remove_file(&self, id: i64) -> Result<()> {
+ // Remove the file from the `files` table,
+ self.0
+ .execute("DELETE FROM files WHERE id = ?", params![id])?;
+ // but also remove it from the table for mapping tags.
+ self.0
+ .execute("DELETE FROM mapping WHERE file = ?", params![id])?;
+ Ok(())
+ }
+
+ /// Return the identifier for `tag`, or `Err` if no such tag exists.
+ fn tag_id<T: AsRef<str>>(&self, tag: T) -> Result<i64> {
Ok(self
.0
- .prepare("SELECT id FROM images WHERE orig_dir = ? AND filename = ?")?
- .query_row(params![path, file_name], |row| Ok(row.get::<_, i64>(0)))??)
+ .prepare("SELECT * FROM tags WHERE name = ?")?
+ .query_row(params![tag.as_ref()], |row| Ok(row.get::<_, i64>(0)))??)
}
- /// Return the `Image` struct for the image with `id`.
- pub fn image_by_id(&self, id: i64) -> Result<Image> {
+ /// Return the identifiers for all tags named similarly to `tag`.
+ ///
+ /// This function treats `tag` as a wildcard, and therefore only makes sense
+ /// in the context of a query. For inserting images to the database, or any
+ /// other action where a "canonical" tag is desired, reach for `tag_id`
+ /// instead.
+ fn tag_ids<T: AsRef<str>>(&self, tag: T) -> Result<Vec<i64>> {
Ok(self
.0
- .prepare(
- "SELECT id, blake2, filename, orig_dir
- FROM images
- WHERE id = ?",
- )?
- .query_row(params![id], |row| {
- Ok(Image {
- id: row.get(0)?,
- blake2: row.get(1)?,
- filename: row.get(2)?,
- orig_dir: row.get(3)?,
- })
- })?)
+ .prepare("SELECT id FROM tags WHERE name LIKE ?")?
+ .query_map(params![tag.as_ref().replace("*", "%")], |row| row.get(0))?
+ .filter_map(|id| id.ok())
+ .collect())
}
- /// Associate `tag` with the image specified by `image_id`.
- pub fn tag_image(&self, image_id: i64, tag: &str) -> Result<()> {
- self.0.execute(
- "INSERT INTO mapping (image, tag) VALUES(?, ?)",
- params![image_id, self.tag_id(tag)?],
- )?;
+ /// Associate all of `tags` with the file named by `id`.
+ ///
+ /// If any tag lacks a row in the database, it will be created. The contents
+ /// of `tags` should not contain wildcardcard expressions.
+ pub fn tag_file<T: AsRef<str>>(&self, id: i64, tags: &Vec<T>) -> Result<()> {
+ for tag in tags.iter() {
+ // Ensure that a row for `tag` into the database.
+ if self.tag_id(tag.as_ref()).is_err() {
+ self.0
+ .execute("INSERT INTO tags (name) VALUES(?)", params![tag.as_ref()])?;
+ }
+ self.0.execute(
+ "INSERT INTO mapping (file, tag) VALUES(?, ?)",
+ params![id, self.tag_id(tag.as_ref()).unwrap()],
+ )?;
+ }
Ok(())
}
- /// Disassociate `tag` with the image specified by `image_id`.
- pub fn untag_image(&self, image_id: i64, tag: &str) -> Result<()> {
- self.0.execute(
- "DELETE FROM mapping WHERE image = ? AND tag = ?",
- params![image_id, self.tag_id(tag)?],
- )?;
+ /// Remove all of `tags` from the file named by `id`.
+ ///
+ /// The contents of `tags` can contain wildcardcard expressions.
+ pub fn untag_file<T: AsRef<str>>(&self, id: i64, tags: &Vec<T>) -> Result<()> {
+ for tag in tags.iter() {
+ // Silently skip any tags which do not exist in the database.
+ if let Ok(tags) = self.tag_ids(tag.as_ref()) {
+ for tag in tags.iter() {
+ self.0
+ .prepare("DELETE FROM mapping WHERE file = ? AND tag = ?")?
+ .insert(params![id, tag])?;
+ }
+ }
+ }
Ok(())
}
- /// Return the tags for the image specified by `image_id`.
- pub fn tags_for_image(&self, image_id: i64) -> Result<Vec<String>> {
+ /// Return the `File` object for the file named by `id`.
+ pub fn file_by_id(&self, id: i64) -> Result<File> {
+ let tags = self.tags_for_file(id)?;
Ok(self
.0
- .prepare(
- "SELECT tags.name
- FROM mapping
- INNER JOIN tags ON mapping.tag = tags.id
- WHERE mapping.image = ?;",
- )?
- .query_map(params![image_id], |row| row.get(0))?
- .filter_map(|tag| tag.ok())
- .collect())
+ .prepare("SELECT id, path FROM files WHERE id = ?")?
+ .query_row(params![id], |row| {
+ Ok(File {
+ id: row.get(0)?,
+ path: row.get(1)?,
+ tags,
+ })
+ })?)
}
- /// Return the intersection of the sets of images matching each of `tags`.
- pub fn query(&self, q: Query) -> Result<Vec<Image>> {
- let ids: Vec<i64> = q
- .tags
- .iter()
- .filter_map(|tag| self.tag_id(tag).ok())
- .collect();
- let mut tags = Vec::from(q.tags);
- tags.dedup();
-
- // Query contained a tag that's not in the database.
- if ids.len() != tags.len() {
- return Ok(vec![]);
+ /// Return the files in the database satisfying `q` and `k`.
+ pub fn query(&self, q: &Query, k: Option<Keyset>) -> Result<Vec<File>> {
+ // Return an empty vector if the query contains tags that aren't in the
+ // database.
+ for part in q.iter() {
+ if let Tag(Is(name)) = part {
+ if self.tag_id(name).is_err() {
+ return Ok(vec![]);
+ }
+ }
}
- let union = if tags.is_empty() {
- None
- } else {
- Some(
- (1..ids.len() + 1)
- .zip(ids)
- .map(|pair| {
- let (n, id) = pair;
- format!(
- "INNER JOIN mapping as m{n}
- ON images.id = m{n}.image
- AND {id} = m{n}.tag",
- n = n,
- id = id
- )
- })
- .collect::<Vec<String>>()
- .join("\n"),
- )
- };
+ let predicates = q
+ .iter()
+ .filter_map(|field| match field {
+ Tag(Is(expression)) => Some(
+ self.tag_ids(expression)
+ .ok()?
+ .iter()
+ .map(|tag| format!("(mapping.tag = {})", tag))
+ .collect::<Vec<_>>(),
+ ),
+ Tag(IsNot(expression)) => Some(
+ self.tag_ids(expression)
+ .ok()?
+ .iter()
+ .map(|tag| {
+ format!(
+ "(files.id NOT IN
+ (SELECT files.id FROM files
+ JOIN mapping ON files.id = mapping.file
+ WHERE mapping.tag = {}))",
+ tag
+ )
+ })
+ .collect::<Vec<_>>(),
+ ),
+ Filename(Is(expression)) => Some(vec![format!(
+ "(file.path LIKE %{})",
+ expression.replace("*", "%")
+ )]),
+ Filename(IsNot(expression)) => Some(vec![format!(
+ "(file.path NOT LIKE %{})",
+ expression.replace("*", "%")
+ )]),
+ })
+ .flat_map(|s| s)
+ .collect::<Vec<_>>();
let query = format!(
- "SELECT id, blake2, filename, orig_dir
- FROM images
- {union}
- {since}
- GROUP BY id
- ORDER BY id DESC
- {limit}",
- since = if let Some(n) = q.last_id {
- format!("WHERE id < {}", n)
+ "SELECT files.id FROM files
+ JOIN mapping
+ ON files.id = mapping.file
+ WHERE {predicate}
+ {since}
+ GROUP BY files.id
+ ORDER BY files.id DESC
+ {limit}",
+ predicate = if predicates.is_empty() {
+ "1 == 1".into()
} else {
- String::from("")
+ predicates.join(" AND ")
},
- limit = if let Some(n) = q.limit {
- format!("LIMIT {}", n)
+ since = if let Some(ref keyset) = k {
+ format!("AND files.id < {}", keyset.last_id)
} else {
- String::from("")
+ "".into()
},
- union = union.or(Some("".into())).unwrap(),
+ limit = if let Some(ref keyset) = k {
+ format!("LIMIT {}", keyset.how_many)
+ } else {
+ "".into()
+ }
);
Ok(self
.0
.prepare(&query)?
- .query_map(
- params![],
- |row| -> rusqlite::Result<(i64, String, String, String)> {
- Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
- },
- )?
- .filter_map(|res| {
- res.map(|pair| {
- let (id, blake2, filename, orig_dir) = pair;
- Image {
- id,
- blake2,
- filename,
- orig_dir,
- }
- })
- .ok()
- })
+ .query_map(params![], |row| row.get::<_, i64>(0))?
+ .filter_map(|id| id.ok().and_then(|id| self.file_by_id(id).ok()))
.collect())
}
- /// Return the intersection of the sets of images matching each of `tags`.
- pub fn images_by_tags(&self, tags: &[&str]) -> Result<Vec<Image>> {
- self.query(Query {
- tags,
- last_id: None,
- limit: None,
- })
- }
-
- /// Return the `n` greatest tags, ordered by number of associated images.
pub fn top_tags(&self, n: i64) -> Result<Vec<(i64, String)>> {
Ok(self
.0
@@ -337,95 +379,210 @@ mod tests {
}
#[test]
- fn tables_exist() {
+ fn parse_query_empty() {
+ let parsed = parse_query("").unwrap();
+ assert!(parsed.len() == 0);
+ }
+
+ #[test]
+ fn parse_query_tags_only() {
+ let parsed = parse_query("tag1 tag2 tag3").unwrap();
+ assert_eq!(parsed.len(), 3);
+ assert!(parsed.contains(&Tag(Is("tag1".into()))));
+ assert!(parsed.contains(&Tag(Is("tag2".into()))));
+ assert!(parsed.contains(&Tag(Is("tag3".into()))));
+ }
+
+ #[test]
+ fn parse_query_tag_negations_only() {
+ let parsed = parse_query("-tag1 -tag2 -tag3").unwrap();
+ assert_eq!(parsed.len(), 3);
+ assert!(parsed.contains(&Tag(IsNot("tag1".into()))));
+ assert!(parsed.contains(&Tag(IsNot("tag2".into()))));
+ assert!(parsed.contains(&Tag(IsNot("tag3".into()))));
+ }
+
+ #[test]
+ fn parse_query_filenames_only() {
+ let parsed = parse_query("filename:1 filename:2 filename:3").unwrap();
+ assert_eq!(parsed.len(), 3);
+ assert!(parsed.contains(&Filename(Is("1".into()))));
+ assert!(parsed.contains(&Filename(Is("2".into()))));
+ assert!(parsed.contains(&Filename(Is("3".into()))));
+ }
+
+ #[test]
+ fn parse_query_filename_negations_only() {
+ let parsed = parse_query("-filename:1 -filename:2 -filename:3").unwrap();
+ assert_eq!(parsed.len(), 3);
+ assert!(parsed.contains(&Filename(IsNot("1".into()))));
+ assert!(parsed.contains(&Filename(IsNot("2".into()))));
+ assert!(parsed.contains(&Filename(IsNot("3".into()))));
+ }
+
+ #[test]
+ fn parse_query_combined() {
+ let parsed = parse_query("tag1 -tag2 filename:1 -filename:2").unwrap();
+ assert_eq!(parsed.len(), 4);
+ assert!(parsed.contains(&Tag(Is("tag1".into()))));
+ assert!(parsed.contains(&Tag(IsNot("tag2".into()))));
+ assert!(parsed.contains(&Filename(Is("1".into()))));
+ assert!(parsed.contains(&Filename(IsNot("2".into()))));
+ }
+
+ #[test]
+ fn test_add_file_fail_on_already_indexed() {
let tb = TagDatabase::new_mem().unwrap();
- for table in ["images", "tags", "mapping"].iter() {
- let exists =
- tb.0.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")
- .unwrap()
- .exists(params![table])
- .unwrap();
- assert!(exists);
- }
+ let img = tb.add_file("test", &vec!["tag1"]).unwrap();
+ assert!(tb.add_file("test", &vec!["tag2"]).is_err());
}
#[test]
- fn insert_file() {
+ fn test_add_file_output_reflects_input() {
let tb = TagDatabase::new_mem().unwrap();
- let id = tb
- .import_image(Path::new("/fake/path"), "fakehash")
- .unwrap();
- let exists =
- tb.0.prepare("SELECT filename, blake2 FROM images WHERE id = ?;")
- .unwrap()
- .exists(params![id])
- .unwrap();
- assert!(exists);
- let exists =
- tb.0.prepare("SELECT filename, blake2 FROM images WHERE id = ?;")
- .unwrap()
- .exists(params![id + 1])
- .unwrap();
- assert!(!exists);
+ let img = tb.add_file("test", &vec!["tag1", "tag2"]).unwrap();
+ assert_eq!(img.path, "test");
+ assert_eq!(img.tags.len(), 2);
+ assert!(img.tags.contains(&String::from("tag1")));
+ assert!(img.tags.contains(&String::from("tag2")));
+ }
+
+ #[test]
+ fn test_remove_file() {
+ let tb = TagDatabase::new_mem().unwrap();
+ let img = tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+ tb.remove_file(img.id).unwrap();
+
+ let query = parse_query("tag1").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 0);
+ }
+
+ #[test]
+ fn test_query_tags_only() {
+ let tb = TagDatabase::new_mem().unwrap();
+ tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+ tb.add_file("test2", &vec!["tag2"]).unwrap();
+
+ let query = parse_query("tag1").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].path, "test1");
+
+ let query = parse_query("tag2").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 2);
+ }
+
+ #[test]
+ fn test_query_tags_not_indexed() {
+ let tb = TagDatabase::new_mem().unwrap();
+ tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+
+ let query = parse_query("tag3").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 0);
}
#[test]
- fn add_tag() {
+ fn test_query_tag_negations_only() {
let tb = TagDatabase::new_mem().unwrap();
- tb.add_tag("test").unwrap();
- tb.tag_id("test").unwrap();
+ tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+ tb.add_file("test2", &vec!["tag2"]).unwrap();
+
+ let query = parse_query("-tag1").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].path, "test2");
}
#[test]
- fn tag_image() {
+ fn test_query_tags_with_keyset() {
let tb = TagDatabase::new_mem().unwrap();
- let id = tb
- .import_image(Path::new("/fake/path"), "fakehash")
+ tb.add_file("test1", &vec!["tag1"]).unwrap();
+ tb.add_file("test2", &vec!["tag1"]).unwrap();
+
+ let query = parse_query("tag1").unwrap();
+ let results = tb
+ .query(
+ &query,
+ Some(Keyset {
+ last_id: 3,
+ how_many: 1,
+ }),
+ )
.unwrap();
- tb.add_tag("test").unwrap();
- tb.tag_image(id, "test").unwrap();
- let tags = tb.tags_for_image(id).unwrap();
- assert_eq!(tags.len(), 1);
- assert!(tags.contains(&String::from("test")));
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].path, "test2");
+
+ let results = tb
+ .query(
+ &query,
+ Some(Keyset {
+ last_id: 2,
+ how_many: 1,
+ }),
+ )
+ .unwrap();
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].path, "test1");
}
#[test]
- fn query_by_tag() {
+ fn test_empty_query() {
let tb = TagDatabase::new_mem().unwrap();
- let ids: Vec<i64> = (1..4)
- .map(|i| {
- let i = i.to_string();
- tb.import_image(Path::new(&i), &i).unwrap()
- })
- .collect();
- tb.add_tag("1").unwrap();
- tb.add_tag("2").unwrap();
- tb.tag_image(ids[0], "1").unwrap();
- tb.tag_image(ids[1], "2").unwrap();
- tb.tag_image(ids[2], "1").unwrap();
- tb.tag_image(ids[2], "2").unwrap();
- assert_eq!(tb.images_by_tags(&["1"]).unwrap().len(), 2);
- assert_eq!(tb.images_by_tags(&["2"]).unwrap().len(), 2);
- assert_eq!(tb.images_by_tags(&["1", "2"]).unwrap().len(), 1);
+ tb.add_file("test1", &vec!["tag1"]).unwrap();
+ tb.add_file("test2", &vec!["tag1"]).unwrap();
+
+ let query = parse_query("").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 2);
+ }
+
+ #[test]
+ fn test_tag_file() {
+ let tb = TagDatabase::new_mem().unwrap();
+ let img = tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+ tb.tag_file(img.id, &vec!["tag3"]).unwrap();
+
+ let query = parse_query("tag3").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 1);
+ assert_eq!(results[0].path, "test1");
+ }
+
+ #[test]
+ fn test_untag_file() {
+ let tb = TagDatabase::new_mem().unwrap();
+ let img = tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+ tb.untag_file(img.id, &vec!["tag2"]).unwrap();
+
+ let query = parse_query("tag2").unwrap();
+ let results = tb.query(&query, None).unwrap();
+ assert_eq!(results.len(), 0);
+ }
+
+ #[test]
+ fn test_id_resolution() {
+ let tb = TagDatabase::new_mem().unwrap();
+ let img = tb.add_file("test1", &vec!["tag1", "tag2"]).unwrap();
+ assert_eq!(tb.file_by_id(img.id).unwrap().path, "test1");
}
#[test]
fn top_tags() {
let tb = TagDatabase::new_mem().unwrap();
- let ids: Vec<i64> = (1..4)
+ let ids: Vec<_> = (1..4)
.map(|i| {
let i = i.to_string();
- tb.import_image(Path::new(&i), &i).unwrap()
+ tb.add_file(Path::new(&i), &Vec::<&str>::new()).unwrap()
})
.collect();
- tb.add_tag("1").unwrap();
- tb.add_tag("2").unwrap();
- tb.tag_image(ids[0], "1").unwrap();
- tb.tag_image(ids[1], "2").unwrap();
- tb.tag_image(ids[2], "1").unwrap();
- tb.tag_image(ids[2], "2").unwrap();
+ tb.tag_file(ids[0].id, &vec!["1"]).unwrap();
+ tb.tag_file(ids[1].id, &vec!["2"]).unwrap();
+ tb.tag_file(ids[2].id, &vec!["1"]).unwrap();
+ tb.tag_file(ids[2].id, &vec!["2"]).unwrap();
assert_eq!(tb.top_tags(2).unwrap().len(), 2);
assert_eq!(tb.top_tags(2).unwrap()[0].0, 2);
- assert_eq!(tb.top_tags(2).unwrap()[1].0, 2);
}
}