Implement filter expressions supporting AND and OR

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 699698a251
commit 3fb98188dc

@ -855,8 +855,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mediarepo-api" name = "mediarepo-api"
version = "0.10.0" version = "0.11.0"
source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=3e8a415a4cf1c04e3e7ccefbcdd56b650ad40c85#3e8a415a4cf1c04e3e7ccefbcdd56b650ad40c85" source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=6fbc72699a565b1ca3f76f58c3dafbe9790c758e#6fbc72699a565b1ca3f76f58c3dafbe9790c758e"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

@ -65,6 +65,9 @@ pub async fn get_hashes_with_tag_count(
db: &DatabaseConnection, db: &DatabaseConnection,
hash_ids: Vec<i64>, hash_ids: Vec<i64>,
) -> RepoResult<HashMap<i64, u32>> { ) -> RepoResult<HashMap<i64, u32>> {
if hash_ids.is_empty() {
return Ok(HashMap::new());
}
let hash_tag_counts: Vec<HashTagCount> = let hash_tag_counts: Vec<HashTagCount> =
HashTagCount::find_by_statement(Statement::from_sql_and_values( HashTagCount::find_by_statement(Statement::from_sql_and_values(
DbBackend::Sqlite, DbBackend::Sqlite,
@ -94,7 +97,9 @@ fn vec_to_query_list<D: Display>(input: Vec<D>) -> String {
let mut entries = input let mut entries = input
.into_iter() .into_iter()
.fold(String::new(), |acc, val| format!("{}{},", acc, val)); .fold(String::new(), |acc, val| format!("{}{},", acc, val));
entries.remove(entries.len() - 1); if entries.len() > 0 {
entries.remove(entries.len() - 1);
}
entries entries
} }

@ -90,36 +90,13 @@ impl File {
#[tracing::instrument(level = "debug", skip(db))] #[tracing::instrument(level = "debug", skip(db))]
pub(crate) async fn find_by_tags( pub(crate) async fn find_by_tags(
db: DatabaseConnection, db: DatabaseConnection,
tag_ids: Vec<(i64, bool)>, tag_ids: Vec<Vec<(i64, bool)>>,
) -> RepoResult<Vec<Self>> { ) -> RepoResult<Vec<Self>> {
let mut condition = Condition::all(); let main_condition = build_find_filter_conditions(tag_ids);
for (tag, negated) in tag_ids {
condition = if negated {
condition.add(
hash::Column::Id.not_in_subquery(
Query::select()
.expr(Expr::col(hash_tag::Column::HashId))
.from(hash_tag::Entity)
.cond_where(hash_tag::Column::TagId.eq(tag))
.to_owned(),
),
)
} else {
condition.add(
hash::Column::Id.in_subquery(
Query::select()
.expr(Expr::col(hash_tag::Column::HashId))
.from(hash_tag::Entity)
.cond_where(hash_tag::Column::TagId.eq(tag))
.to_owned(),
),
)
}
}
let results: Vec<(hash::Model, Option<file::Model>)> = hash::Entity::find() let results: Vec<(hash::Model, Option<file::Model>)> = hash::Entity::find()
.find_also_related(file::Entity) .find_also_related(file::Entity)
.filter(condition) .filter(main_condition)
.group_by(file::Column::Id) .group_by(file::Column::Id)
.all(&db) .all(&db)
.await?; .await?;
@ -373,3 +350,46 @@ impl File {
} }
} }
} }
fn build_find_filter_conditions(tag_ids: Vec<Vec<(i64, bool)>>) -> Condition {
let mut main_condition = Condition::all();
for mut expression in tag_ids {
if expression.len() == 1 {
let (tag_id, negated) = expression.pop().unwrap();
main_condition = add_single_filter_expression(main_condition, tag_id, negated)
} else if !expression.is_empty() {
let mut sub_condition = Condition::any();
for (tag, negated) in expression {
sub_condition = add_single_filter_expression(sub_condition, tag, negated);
}
main_condition = main_condition.add(sub_condition);
}
}
main_condition
}
fn add_single_filter_expression(condition: Condition, tag_id: i64, negated: bool) -> Condition {
if negated {
condition.add(
hash::Column::Id.not_in_subquery(
Query::select()
.expr(Expr::col(hash_tag::Column::HashId))
.from(hash_tag::Entity)
.cond_where(hash_tag::Column::TagId.eq(tag_id))
.to_owned(),
),
)
} else {
condition.add(
hash::Column::Id.in_subquery(
Query::select()
.expr(Expr::col(hash_tag::Column::HashId))
.from(hash_tag::Entity)
.cond_where(hash_tag::Column::TagId.eq(tag_id))
.to_owned(),
),
)
}
}

@ -108,25 +108,26 @@ impl Repo {
/// Finds all files by a list of tags /// Finds all files by a list of tags
#[tracing::instrument(level = "debug", skip(self))] #[tracing::instrument(level = "debug", skip(self))]
pub async fn find_files_by_tags(&self, tags: Vec<(String, bool)>) -> RepoResult<Vec<File>> { pub async fn find_files_by_tags(
&self,
tags: Vec<Vec<(String, bool)>>,
) -> RepoResult<Vec<File>> {
let parsed_tags = tags let parsed_tags = tags
.iter() .iter()
.map(|t| parse_namespace_and_tag(t.0.clone())) .flat_map(|e| e.into_iter().map(|t| parse_namespace_and_tag(t.0.clone())))
.unique()
.collect(); .collect();
let db_tags = self.tags_by_names(parsed_tags).await?; let db_tags = self.tags_by_names(parsed_tags).await?;
let tag_map: HashMap<String, bool> = HashMap::from_iter(tags.into_iter()); let tag_map: HashMap<String, i64> =
HashMap::from_iter(db_tags.into_iter().map(|t| (t.normalized_name(), t.id())));
let tag_ids: Vec<(i64, bool)> = db_tags let tag_ids: Vec<Vec<(i64, bool)>> = tags
.into_iter() .into_iter()
.map(|tag| { .map(|expr| {
( expr.into_iter()
tag.id(), .filter_map(|(tag, negated)| Some((*tag_map.get(&tag)?, negated)))
tag_map .collect_vec()
.get(&tag.normalized_name())
.cloned()
.unwrap_or(false),
)
}) })
.collect(); .collect();

@ -802,8 +802,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mediarepo-api" name = "mediarepo-api"
version = "0.10.0" version = "0.11.0"
source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=3e8a415a4cf1c04e3e7ccefbcdd56b650ad40c85#3e8a415a4cf1c04e3e7ccefbcdd56b650ad40c85" source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=6fbc72699a565b1ca3f76f58c3dafbe9790c758e#6fbc72699a565b1ca3f76f58c3dafbe9790c758e"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

@ -34,4 +34,4 @@ features = ["tokio-executor"]
[dependencies.mediarepo-api] [dependencies.mediarepo-api]
git = "https://github.com/Trivernis/mediarepo-api.git" git = "https://github.com/Trivernis/mediarepo-api.git"
rev = "3e8a415a4cf1c04e3e7ccefbcdd56b650ad40c85" rev = "6fbc72699a565b1ca3f76f58c3dafbe9790c758e"

@ -3,7 +3,7 @@ use crate::utils::{file_by_identifier, get_repo_from_context, hash_by_identifier
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use compare::Compare; use compare::Compare;
use mediarepo_api::types::files::{ use mediarepo_api::types::files::{
AddFileRequestHeader, FileMetadataResponse, FindFilesByTagsRequest, AddFileRequestHeader, FileMetadataResponse, FilterExpression, FindFilesRequest,
GetFileThumbnailOfSizeRequest, GetFileThumbnailsRequest, ReadFileRequest, SortDirection, GetFileThumbnailOfSizeRequest, GetFileThumbnailsRequest, ReadFileRequest, SortDirection,
SortKey, ThumbnailMetadataResponse, UpdateFileNameRequest, SortKey, ThumbnailMetadataResponse, UpdateFileNameRequest,
}; };
@ -87,9 +87,22 @@ impl FilesNamespace {
/// Searches for files by tags /// Searches for files by tags
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
async fn find_files<S: AsyncProtocolStream>(ctx: &Context<S>, event: Event) -> IPCResult<()> { async fn find_files<S: AsyncProtocolStream>(ctx: &Context<S>, event: Event) -> IPCResult<()> {
let req = event.data::<FindFilesByTagsRequest>()?; let req = event.data::<FindFilesRequest>()?;
let repo = get_repo_from_context(ctx).await; let repo = get_repo_from_context(ctx).await;
let tags = req.tags.into_iter().map(|t| (t.name, t.negate)).collect();
let tags = req
.filters
.into_iter()
.map(|e| match e {
FilterExpression::OrExpression(tags) => {
tags.into_iter().map(|t| (t.tag, t.negate)).collect_vec()
}
FilterExpression::Query(tag) => {
vec![(tag.tag, tag.negate)]
}
})
.collect();
let mut files = repo.find_files_by_tags(tags).await?; let mut files = repo.find_files_by_tags(tags).await?;
let hash_ids: Vec<i64> = files.iter().map(|f| f.hash_id()).collect(); let hash_ids: Vec<i64> = files.iter().map(|f| f.hash_id()).collect();

Loading…
Cancel
Save