Database schema optimization - part 2

This commit is contained in:
mdecimus
2025-10-31 18:25:01 +01:00
parent 3e1d2d54e4
commit d5c5f12adf
43 changed files with 2266 additions and 1853 deletions

View File

@@ -8,7 +8,7 @@ use calcard::jscalendar::JSCalendarProperty;
use common::Server;
use jmap_proto::error::set::SetError;
use trc::AddContext;
use types::{collection::Collection, field::CalendarField, id::Id};
use types::{collection::Collection, field::CalendarEventField, id::Id};
pub mod copy;
pub mod get;
@@ -67,7 +67,7 @@ pub(super) async fn assert_is_unique_uid(
.document_exists(
account_id,
Collection::CalendarEvent,
CalendarField::Uid,
CalendarEventField::Uid,
uid.as_bytes(),
)
.await

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::changes::state::JmapCacheState;
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use calcard::{common::timezone::Tz, jscalendar::JSCalendarDateTime};
use chrono::offset::TimeZone;
use common::{Server, auth::AccessToken};
@@ -14,15 +14,17 @@ use jmap_proto::{
object::calendar_event::{self, CalendarEventComparator, CalendarEventFilter},
request::MaybeInvalid,
};
use nlp::tokenizers::word::WordTokenizer;
use nlp::language::Language;
use std::{cmp::Ordering, sync::Arc};
use store::{backend::MAX_TOKEN_LENGTH, roaring::RoaringBitmap, search::SearchFilter};
use store::{
roaring::RoaringBitmap,
search::{CalendarSearchField, SearchComparator, SearchFilter},
};
use trc::AddContext;
use types::{
TimeRange,
acl::Acl,
collection::{Collection, SyncCollection},
field::CalendarField,
};
pub trait CalendarEventQuery: Sync + Send {
@@ -44,10 +46,7 @@ impl CalendarEventQuery for Server {
let cache = self
.fetch_dav_resources(access_token, account_id, SyncCollection::Calendar)
.await?;
let filter_mask = (access_token.is_shared(account_id))
.then(|| cache.shared_items(access_token, [Acl::ReadItems], true));
let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC);
let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false);
let mut filter: Option<TimeRange> = None;
let mut did_filter_by_time = false;
@@ -73,15 +72,73 @@ impl CalendarEventQuery for Server {
)))
}
CalendarEventFilter::Uid(uid) => {
filters.push(SearchFilter::eq(CalendarField::Uid, uid.into_bytes()))
filters.push(SearchFilter::eq(CalendarSearchField::Uid, uid));
}
CalendarEventFilter::Text(value) => {
for token in WordTokenizer::new(&value, MAX_TOKEN_LENGTH) {
filters.push(SearchFilter::eq(
CalendarField::Text,
token.word.into_owned().into_bytes(),
));
}
let (text, language) =
Language::detect(value, self.core.jmap.default_language);
filters.push(SearchFilter::Or);
filters.push(SearchFilter::has_text(
CalendarSearchField::Title,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Description,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Location,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Owner,
text.clone(),
language,
));
filters.push(SearchFilter::has_text(
CalendarSearchField::Attendee,
text,
language,
));
filters.push(SearchFilter::End);
}
CalendarEventFilter::Title(title) => {
filters.push(SearchFilter::has_text_detect(
CalendarSearchField::Title,
title,
self.core.jmap.default_language,
));
}
CalendarEventFilter::Description(description) => {
filters.push(SearchFilter::has_text_detect(
CalendarSearchField::Description,
description,
self.core.jmap.default_language,
));
}
CalendarEventFilter::Location(location) => {
filters.push(SearchFilter::has_text_detect(
CalendarSearchField::Location,
location,
self.core.jmap.default_language,
));
}
CalendarEventFilter::Owner(owner) => {
filters.push(SearchFilter::has_text(
CalendarSearchField::Owner,
owner,
Language::None,
));
}
CalendarEventFilter::Attendee(attendee) => {
filters.push(SearchFilter::has_text(
CalendarSearchField::Attendee,
attendee,
Language::None,
));
}
CalendarEventFilter::After(_) | CalendarEventFilter::Before(_) => {
if let Some(filter) = &filter
@@ -105,29 +162,74 @@ impl CalendarEventQuery for Server {
.details(unsupported.into_string()));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let mut result_set = self
.filter(account_id, Collection::CalendarEvent, filters)
let expand_recurrences = request.arguments.expand_recurrences.unwrap_or(false);
let comparators = if !expand_recurrences {
request
.sort
.take()
.unwrap_or_default()
.into_iter()
.map(|comparator| match comparator.property {
CalendarEventComparator::Start | CalendarEventComparator::RecurrenceId => {
Ok(SearchComparator::field(
CalendarSearchField::Start,
comparator.is_ascending,
))
}
CalendarEventComparator::Uid => Ok(SearchComparator::field(
CalendarSearchField::Uid,
comparator.is_ascending,
)),
CalendarEventComparator::Created | CalendarEventComparator::Updated => {
Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(comparator.property.into_string().into_owned()))
}
CalendarEventComparator::_T(other) => Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(other.to_string())),
})
.collect::<Result<Vec<_>, _>>()?
} else {
vec![]
};
let results = self
.search_store()
.query(account_id, Collection::CalendarEvent, filters, comparators)
.await?;
if let Some(filter_mask) = filter_mask {
result_set.apply_mask(filter_mask);
}
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
cache.get_state(false),
&request,
);
let num_results = result_set.results.len() as usize;
if num_results > 0 {
if !results.is_empty() {
// Extract comparators
let comparators = request
.sort
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or_default();
let filter_mask = (access_token.is_shared(account_id))
.then(|| cache.shared_items(access_token, [Acl::ReadItems], true));
if expand_recurrences {
let Some(time_range) = filter.filter(|f| f.start != i64::MIN && f.end != i64::MAX)
else {
@@ -136,12 +238,19 @@ impl CalendarEventQuery for Server {
));
};
let max_instances = self.core.groupware.max_ical_instances;
let mut results = Vec::with_capacity(result_set.results.len() as usize);
let mut expanded_results = Vec::with_capacity(results.len() as usize);
let has_uid_comparator = comparators
.iter()
.any(|c| matches!(c.property, CalendarEventComparator::Uid));
for document_id in result_set.results {
for document_id in results {
if filter_mask
.as_ref()
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
{
continue;
}
let Some(_calendar_event) = self
.archive(account_id, Collection::CalendarEvent, document_id)
.await?
@@ -171,14 +280,14 @@ impl CalendarEventQuery for Server {
.expand(default_tz, time_range)
.unwrap_or_default()
{
if results.len() < max_instances {
results.push(SearchResult {
if expanded_results.len() < max_instances {
expanded_results.push(SearchResult {
created: calendar_event.created.to_native().to_be_bytes(),
updated: calendar_event.modified.to_native().to_be_bytes(),
start: expansion.start.to_be_bytes(),
uid: uid.clone(),
document_id,
expansion_id: expansion.expansion_id,
expansion_id: expansion.expansion_id.into(),
});
} else {
return Err(trc::JmapEvent::InvalidArguments.into_err().details(
@@ -189,8 +298,8 @@ impl CalendarEventQuery for Server {
}
// Sort results
if !results.is_empty() {
results.sort_by(|a, b| {
if !expanded_results.is_empty() {
expanded_results.sort_by(|a, b| {
for comparator in comparators {
let ordering = a
.get_property(&comparator.property)
@@ -208,69 +317,30 @@ impl CalendarEventQuery for Server {
}
Ordering::Equal
});
}
// Add results
let (mut response, paginate) = self
.build_query_response(results.len(), cache.get_state(false), &request)
.await?;
if let Some(mut paginate) = paginate {
for result in results {
if !paginate.add(result.expansion_id + 1, result.document_id) {
// Add results
for result in expanded_results {
if !response.add(result.expansion_id.unwrap() + 1, result.document_id) {
break;
}
}
response.update_results(paginate.build())?;
}
Ok(response)
} else {
let mut comparators_ = Vec::with_capacity(comparators.len());
for comparator in comparators {
comparators_.push(match &comparator.property {
CalendarEventComparator::Uid => {
SearchComparator::field(CalendarField::Uid, comparator.is_ascending)
}
CalendarEventComparator::Start => {
SearchComparator::field(CalendarField::Start, comparator.is_ascending)
}
CalendarEventComparator::Created => {
SearchComparator::field(CalendarField::Created, comparator.is_ascending)
}
CalendarEventComparator::Updated => {
SearchComparator::field(CalendarField::Updated, comparator.is_ascending)
}
unsupported => {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(unsupported.clone().into_string()));
}
});
}
// Sort results
let (response, paginate) = self
.build_query_response(num_results, cache.get_state(false), &request)
.await?;
if let Some(paginate) = paginate {
self.sort(result_set, comparators_, paginate, response)
.await
} else {
Ok(response)
for document_id in results {
if filter_mask
.as_ref()
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
{
continue;
}
if !response.add(0, document_id) {
break;
}
}
}
} else {
let (response, _) = self
.build_query_response(
result_set.results.len() as usize,
cache.get_state(false),
&request,
)
.await?;
Ok(response)
}
response.build()
}
}
@@ -282,7 +352,7 @@ fn local_timestamp(dt: &JSCalendarDateTime, tz: Tz) -> Option<i64> {
#[derive(Debug)]
struct SearchResult {
expansion_id: u32,
expansion_id: Option<u32>,
document_id: u32,
start: [u8; std::mem::size_of::<i64>()],
created: [u8; std::mem::size_of::<i64>()],

View File

@@ -4,21 +4,28 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{ changes::state::JmapCacheState};
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use common::{Server, auth::AccessToken};
use groupware::cache::GroupwareCache;
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
method::query::{Filter, QueryRequest, QueryResponse},
object::calendar_event_notification::{
CalendarEventNotification, CalendarEventNotificationComparator,
CalendarEventNotificationFilter,
},
request::IntoValid,
};
use store::{SerializeInfallible, query};
use store::{
IterateParams, U32_LEN, U64_LEN, ValueKey,
ahash::AHashSet,
roaring::RoaringBitmap,
search::SearchFilter,
write::{IndexPropertyClass, ValueClass, key::DeserializeBigEndian},
};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
field::CalendarField,
field::CalendarNotificationField,
};
pub trait CalendarEventNotificationQuery: Sync + Send {
@@ -29,6 +36,12 @@ pub trait CalendarEventNotificationQuery: Sync + Send {
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
}
struct Notification {
document_id: u32,
created: u64,
event_id: u32,
}
impl CalendarEventNotificationQuery for Server {
async fn calendar_event_notification_query(
&self,
@@ -44,36 +57,73 @@ impl CalendarEventNotificationQuery for Server {
SyncCollection::CalendarEventNotification,
)
.await?;
let mut notifications = Vec::with_capacity(16);
self.store()
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: 0,
}),
},
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: CalendarNotificationField::CreatedToId.into(),
value: u64::MAX,
}),
},
)
.ascending(),
|key, value| {
notifications.push(Notification {
document_id: key.deserialize_be_u32(key.len() - U32_LEN)?,
created: key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?,
event_id: value.deserialize_be_u32(0)?,
});
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Property(cond) => match cond {
CalendarEventNotificationFilter::Before(before) => {
filters.push(SearchFilter::lt(
CalendarField::Created,
(before.timestamp() as u64).serialize(),
))
let before = before.timestamp() as u64;
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
notifications
.iter()
.filter_map(|n| (n.created < before).then_some(n.document_id)),
)))
}
CalendarEventNotificationFilter::After(after) => {
filters.push(SearchFilter::gt(
CalendarField::Created,
(after.timestamp() as u64).serialize(),
))
let after = after.timestamp() as u64;
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
notifications
.iter()
.filter_map(|n| (n.created > after).then_some(n.document_id)),
)))
}
CalendarEventNotificationFilter::CalendarEventIds(ids) => {
let has_many = ids.len() > 1;
if has_many {
filters.push(SearchFilter::Or);
}
for id in ids.into_valid() {
filters.push(SearchFilter::eq(
CalendarField::EventId,
id.document_id().serialize(),
));
}
if has_many {
filters.push(SearchFilter::End);
}
let ids = ids
.into_valid()
.map(|id| id.document_id())
.collect::<AHashSet<_>>();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
notifications
.iter()
.filter_map(|n| ids.contains(&n.event_id).then_some(n.document_id)),
)))
}
unsupported => {
return Err(trc::JmapEvent::UnsupportedFilter
@@ -81,49 +131,67 @@ impl CalendarEventNotificationQuery for Server {
.details(unsupported.into_string()));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let result_set = self
.filter(account_id, Collection::CalendarEventNotification, filters)
.await?;
// Parse sort criteria
let mut is_ascending = true;
for comparator in request.sort.take().unwrap_or_default() {
match comparator.property {
CalendarEventNotificationComparator::Created => {
is_ascending = comparator.is_ascending;
}
CalendarEventNotificationComparator::_T(unsupported) => {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(unsupported));
}
};
}
if !is_ascending {
notifications.reverse();
}
let (response, paginate) = self
.build_query_response(
result_set.results.len() as usize,
cache.get_state(false),
&request,
let results = self
.search_store()
.query(
account_id,
Collection::CalendarEventNotification,
filters,
vec![],
)
.await?;
if let Some(paginate) = paginate {
// Parse sort criteria
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
for comparator in request.sort.filter(|s| !s.is_empty()).unwrap_or_else(|| {
vec![Comparator::descending(
CalendarEventNotificationComparator::Created,
)]
}) {
comparators.push(match comparator.property {
CalendarEventNotificationComparator::Created => {
SearchComparator::field(CalendarField::Created, comparator.is_ascending)
}
CalendarEventNotificationComparator::_T(unsupported) => {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(unsupported));
}
});
}
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
cache.get_state(false),
&request,
);
// Sort results
self.sort(result_set, comparators, paginate, response).await
} else {
Ok(response)
if !results.is_empty() {
let results = results.into_iter().collect::<AHashSet<_>>();
for notification in notifications {
if results.contains(&notification.document_id)
&& !response.add(0, notification.document_id)
{
break;
}
}
}
response.build()
}
}

View File

@@ -7,7 +7,6 @@
use calcard::jscontact::JSContactProperty;
use common::{DavName, DavResources, Server};
use jmap_proto::error::set::SetError;
use store::SearchFilter;
use trc::AddContext;
use types::{collection::Collection, field::ContactField, id::Id};
@@ -26,15 +25,15 @@ pub(super) async fn assert_is_unique_uid(
) -> trc::Result<Result<(), SetError<JSContactProperty<Id>>>> {
if let Some(uid) = uid {
let hits = server
.store()
.filter(
.document_ids_matching(
account_id,
Collection::ContactCard,
vec![Filter::eq(ContactField::Uid, uid.as_bytes().to_vec())],
ContactField::Uid,
uid.as_bytes(),
)
.await
.caused_by(trc::location!())?;
if !hits.results.is_empty() {
if !hits.is_empty() {
for document_id in resources
.paths
.iter()
@@ -44,7 +43,7 @@ pub(super) async fn assert_is_unique_uid(
})
.map(|path| resources.resources[path.resource_idx].document_id)
{
if hits.results.contains(document_id) {
if hits.contains(document_id) {
return Ok(Err(SetError::invalid_properties()
.with_property(JSContactProperty::Uid)
.with_description(format!(

View File

@@ -4,24 +4,24 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use common::{Server, auth::AccessToken};
use groupware::cache::GroupwareCache;
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
method::query::{Filter, QueryRequest, QueryResponse},
object::contact::{ContactCard, ContactCardComparator, ContactCardFilter},
request::MaybeInvalid,
};
use nlp::tokenizers::word::WordTokenizer;
use store::{SerializeInfallible, backend::MAX_TOKEN_LENGTH, query, roaring::RoaringBitmap};
use store::{
roaring::RoaringBitmap,
search::{ContactSearchField, SearchComparator, SearchFilter},
};
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
field::ContactField,
};
use utils::sanitize_email;
use crate::{ changes::state::JmapCacheState};
pub trait ContactCardQuery: Sync + Send {
fn contact_card_query(
&self,
@@ -52,93 +52,188 @@ impl ContactCardQuery for Server {
cache.children_ids(id.document_id()),
)))
}
ContactCardFilter::Uid(uid) => {
filters.push(SearchFilter::eq(ContactField::Uid, uid.into_bytes()))
ContactCardFilter::Name(value)
| ContactCardFilter::NameGiven(value)
| ContactCardFilter::NameSurname(value)
| ContactCardFilter::NameSurname2(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Name,
value,
));
}
ContactCardFilter::Nickname(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Nickname,
value,
));
}
ContactCardFilter::Organization(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Organization,
value,
));
}
ContactCardFilter::Phone(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Phone,
value,
));
}
ContactCardFilter::OnlineService(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::OnlineService,
value,
));
}
ContactCardFilter::Address(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Address,
value,
));
}
ContactCardFilter::Note(value) => {
filters.push(SearchFilter::has_text_detect(
ContactSearchField::Note,
value,
self.core.jmap.default_language,
));
}
ContactCardFilter::HasMember(value) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Member,
value,
));
}
ContactCardFilter::Kind(value) => {
filters.push(SearchFilter::eq(ContactSearchField::Kind, value));
}
ContactCardFilter::Uid(value) => {
filters.push(SearchFilter::eq(ContactSearchField::Uid, value))
}
ContactCardFilter::Email(email) => {
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Email,
sanitize_email(&email).unwrap_or(email),
))
}
ContactCardFilter::Email(email) => filters.push(SearchFilter::eq(
ContactField::Email,
sanitize_email(&email).unwrap_or(email).into_bytes(),
)),
ContactCardFilter::Text(value) => {
for token in WordTokenizer::new(&value, MAX_TOKEN_LENGTH) {
filters.push(SearchFilter::eq(
ContactField::Text,
token.word.into_owned().into_bytes(),
));
}
filters.push(SearchFilter::Or);
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Name,
value.clone(),
));
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Nickname,
value.clone(),
));
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Organization,
value.clone(),
));
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Email,
value.clone(),
));
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Phone,
value.clone(),
));
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::OnlineService,
value.clone(),
));
filters.push(SearchFilter::has_unknown_text(
ContactSearchField::Address,
value.clone(),
));
filters.push(SearchFilter::has_text_detect(
ContactSearchField::Note,
value,
self.core.jmap.default_language,
));
filters.push(SearchFilter::End);
}
ContactCardFilter::CreatedBefore(before) => filters.push(SearchFilter::lt(
ContactField::Created,
(before.timestamp() as u64).serialize(),
ContactSearchField::Created,
before.timestamp(),
)),
ContactCardFilter::CreatedAfter(after) => filters.push(SearchFilter::gt(
ContactField::Created,
(after.timestamp() as u64).serialize(),
ContactSearchField::Created,
after.timestamp(),
)),
ContactCardFilter::UpdatedBefore(before) => filters.push(SearchFilter::lt(
ContactField::Updated,
(before.timestamp() as u64).serialize(),
/*ContactCardFilter::UpdatedBefore(before) => filters.push(SearchFilter::lt(
ContactSearchField::Updated,
before.timestamp(),
)),
ContactCardFilter::UpdatedAfter(after) => filters.push(SearchFilter::gt(
ContactField::Updated,
(after.timestamp() as u64).serialize(),
)),
ContactSearchField::Updated,
after.timestamp(),
)),*/
unsupported => {
return Err(trc::JmapEvent::UnsupportedFilter
.into_err()
.details(unsupported.into_string()));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let mut result_set = self
.filter(account_id, Collection::ContactCard, filters)
let comparators = request
.sort
.take()
.unwrap_or_default()
.into_iter()
.map(|comparator| match comparator.property {
ContactCardComparator::Created => Ok(SearchComparator::field(
ContactSearchField::Created,
comparator.is_ascending,
)),
/*ContactCardComparator::Updated => Ok(SearchComparator::field(
ContactSearchField::Updated,
comparator.is_ascending,
)),*/
other => Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(other.into_string())),
})
.collect::<Result<Vec<_>, _>>()?;
let results = self
.search_store()
.query(account_id, Collection::ContactCard, filters, comparators)
.await?;
if let Some(filter_mask) = filter_mask {
result_set.apply_mask(filter_mask);
}
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
cache.get_state(false),
&request,
);
let (response, paginate) = self
.build_query_response(
result_set.results.len() as usize,
cache.get_state(false),
&request,
)
.await?;
if let Some(paginate) = paginate {
// Parse sort criteria
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
for comparator in request
.sort
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![Comparator::descending(ContactCardComparator::Updated)])
for document_id in results {
if filter_mask
.as_ref()
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
{
comparators.push(match comparator.property {
ContactCardComparator::Created => {
SearchComparator::field(ContactField::Created, comparator.is_ascending)
}
ContactCardComparator::Updated => {
SearchComparator::field(ContactField::Updated, comparator.is_ascending)
}
unsupported => {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(unsupported.into_string()));
}
});
continue;
}
if !response.add(0, document_id) {
break;
}
// Sort results
self.sort(result_set, comparators, paginate, response).await
} else {
Ok(response)
}
response.build()
}
}

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{ changes::state::JmapCacheState};
use crate::{api::query::QueryResponseBuilder, changes::state::JmapCacheState};
use common::{Server, auth::AccessToken};
use groupware::cache::GroupwareCache;
use jmap_proto::{
@@ -12,7 +12,7 @@ use jmap_proto::{
object::file_node::{FileNode, FileNodeFilter},
request::MaybeInvalid,
};
use store::{query, roaring::RoaringBitmap};
use store::{roaring::RoaringBitmap, search::SearchFilter};
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
@@ -122,63 +122,51 @@ impl FileNodeQuery for Server {
.details(unsupported.into_string()));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let mut result_set = self
.filter(account_id, Collection::FileNode, filters)
.await?;
if let Some(filter_mask) = filter_mask {
result_set.apply_mask(filter_mask);
if request.sort.as_ref().is_some_and(|s| !s.is_empty()) {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details("Sorting is not supported on FileNode"));
}
let (response, paginate) = self
.build_query_response(
result_set.results.len() as usize,
cache.get_state(false),
&request,
)
let results = self
.search_store()
.query(account_id, Collection::FileNode, filters, vec![])
.await?;
if let Some(paginate) = paginate {
// Parse sort criteria
/*let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
for comparator in request
.sort
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![Comparator::descending(FileNodeComparator::Updated)])
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
cache.get_state(false),
&request,
);
for document_id in results {
if filter_mask
.as_ref()
.is_some_and(|filter_ids| !filter_ids.contains(document_id))
{
comparators.push(match comparator.property {
FileNodeComparator::Created => {
SearchComparator::field(ContactField::Created, comparator.is_ascending)
}
FileNodeComparator::Updated => {
SearchComparator::field(ContactField::Updated, comparator.is_ascending)
}
unsupported => {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details(unsupported.into_string()));
}
});
}*/
if request.sort.is_some_and(|s| !s.is_empty()) {
return Err(trc::JmapEvent::UnsupportedSort
.into_err()
.details("Sorting is not supported on FileNode"));
continue;
}
if !response.add(0, document_id) {
break;
}
// Sort results
self.sort(result_set, Default::default(), paginate, response)
.await
} else {
Ok(response)
}
response.build()
}
}

View File

@@ -108,7 +108,7 @@ impl MailboxQuery for Server {
.items
.iter()
.filter(|mailbox| {
!matches!(mailbox.role, SpecialUse::None) == has_role
matches!(mailbox.role, SpecialUse::None) != has_role
})
.map(|m| m.document_id)
.collect::<RoaringBitmap>(),

View File

@@ -4,7 +4,6 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::JmapMethods;
use common::{Server, auth::AccessToken};
use directory::{Permission, QueryParams, Type, backend::internal::manage::ManageDirectory};
use http_proto::HttpSessionData;
@@ -14,10 +13,12 @@ use jmap_proto::{
types::state::State,
};
use std::future::Future;
use store::{query::ResultSet, roaring::RoaringBitmap};
use store::{roaring::RoaringBitmap, search::SearchFilter};
use trc::AddContext;
use types::collection::Collection;
use crate::api::query::QueryResponseBuilder;
pub trait PrincipalQuery: Sync + Send {
fn principal_query(
&self,
@@ -42,12 +43,6 @@ impl PrincipalQuery for Server {
.details("The administrator has disabled directory queries.".to_string()));
}
let mut result_set = ResultSet {
account_id: request.account_id.document_id(),
collection: Collection::Principal,
results: RoaringBitmap::new(),
};
let mut is_set = true;
let principal_ids = self
.store()
.list_principals(
@@ -70,6 +65,7 @@ impl PrincipalQuery for Server {
.map(|p| p.id())
.collect::<RoaringBitmap>();
let mut filters = Vec::with_capacity(request.filter.len());
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Property(cond) => match cond {
@@ -81,74 +77,52 @@ impl PrincipalQuery for Server {
.query(QueryParams::name(name.as_str()).with_return_member_of(false))
.await?
{
if is_set || result_set.results.contains(principal.id()) {
result_set.results =
RoaringBitmap::from_sorted_iter([principal.id()]).unwrap();
} else {
result_set.results = RoaringBitmap::new();
}
} else {
result_set.results = RoaringBitmap::new();
filters.push(SearchFilter::is_in_set(
RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(),
));
}
is_set = false;
}
PrincipalFilter::Email(email) => {
let mut ids = RoaringBitmap::new();
if let Some(id) = self
.email_to_id(self.directory(), &email, session.session_id)
.await?
{
ids.insert(id);
}
if is_set {
result_set.results = ids;
is_set = false;
} else {
result_set.results &= ids;
filters.push(SearchFilter::is_in_set(
RoaringBitmap::from_sorted_iter([id]).unwrap(),
));
}
}
PrincipalFilter::AccountIds(ids) => {
let ids = ids
.into_iter()
.filter_map(|id| {
let id = id.document_id();
if principal_ids.contains(id) {
Some(id)
} else {
None
}
})
.collect::<RoaringBitmap>();
if is_set {
result_set.results = ids;
is_set = false;
} else {
result_set.results &= ids;
}
filters.push(SearchFilter::is_in_set(
ids.into_iter()
.filter_map(|id| {
let id = id.document_id();
if principal_ids.contains(id) {
Some(id)
} else {
None
}
})
.collect::<RoaringBitmap>(),
));
}
PrincipalFilter::Text(text) => {
let ids = self
.store()
.list_principals(
Some(text.as_str()),
access_token.tenant.map(|t| t.id),
&[],
false,
0,
0,
)
.await?
.items
.into_iter()
.map(|p| p.id())
.collect::<RoaringBitmap>();
if is_set {
result_set.results = ids;
is_set = false;
} else {
result_set.results &= ids;
}
filters.push(SearchFilter::is_in_set(
self.store()
.list_principals(
Some(text.as_str()),
access_token.tenant.map(|t| t.id),
&[],
false,
0,
0,
)
.await?
.items
.into_iter()
.map(|p| p.id())
.collect::<RoaringBitmap>(),
));
}
PrincipalFilter::Type(principal_type) => {
let typ = match principal_type {
@@ -159,28 +133,22 @@ impl PrincipalQuery for Server {
PrincipalType::Other => Type::Other,
};
let ids = self
.store()
.list_principals(
None,
access_token.tenant.map(|t| t.id),
&[typ],
false,
0,
0,
)
.await?
.items
.into_iter()
.map(|p| p.id())
.collect::<RoaringBitmap>();
if is_set {
result_set.results = ids;
is_set = false;
} else {
result_set.results &= ids;
}
filters.push(SearchFilter::is_in_set(
self.store()
.list_principals(
None,
access_token.tenant.map(|t| t.id),
&[typ],
false,
0,
0,
)
.await?
.items
.into_iter()
.map(|p| p.id())
.collect::<RoaringBitmap>(),
));
}
other => {
return Err(trc::JmapEvent::UnsupportedFilter
@@ -188,28 +156,39 @@ impl PrincipalQuery for Server {
.details(other.to_string()));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
return Err(trc::JmapEvent::UnsupportedFilter
.into_err()
.details("Logical operators are not supported"));
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
if is_set {
result_set.results = principal_ids;
} else {
result_set.results &= principal_ids;
}
let (response, paginate) = self
.build_query_response(result_set.results.len() as usize, State::Initial, &request)
let results = self
.search_store()
.query(u32::MAX, Collection::Principal, filters, vec![])
.await?;
if let Some(paginate) = paginate {
self.sort(result_set, Vec::new(), paginate, response).await
} else {
Ok(response)
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
State::Initial,
&request,
);
for document_id in results {
if principal_ids.contains(document_id) && !response.add(0, document_id) {
break;
}
}
response.build()
}
}

View File

@@ -4,18 +4,15 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Duration;
use crate::{ UpdateResults};
use crate::api::query::QueryResponseBuilder;
use common::{Server, sharing::notification::ShareNotification};
use jmap_proto::{
method::query::{Filter, QueryRequest, QueryResponse},
object::share_notification::{self, ShareNotificationFilter},
types::state::State,
};
use store::{
Deserialize, IterateParams, LogKey, U64_LEN, query::ResultSet, write::key::DeserializeBigEndian,
};
use std::time::Duration;
use store::{Deserialize, IterateParams, LogKey, U64_LEN, write::key::DeserializeBigEndian};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
@@ -79,12 +76,6 @@ impl ShareNotificationQuery for Server {
}
let mut results = Vec::new();
let mut result_set = ResultSet {
account_id,
collection: Collection::None,
results: Default::default(),
};
self.store()
.iterate(
IterateParams::new(
@@ -113,7 +104,6 @@ impl ShareNotificationQuery for Server {
}
}
result_set.results.insert(results.len() as u32);
results.push(Id::from(change_id));
Ok(true)
@@ -122,20 +112,19 @@ impl ShareNotificationQuery for Server {
.await
.caused_by(trc::location!())?;
let (mut response, paginate) = self
.build_query_response(result_set.results.len() as usize, State::Initial, &request)
.await?;
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
State::Initial,
&request,
);
if let Some(mut paginate) = paginate {
for result in results {
if !paginate.add_id(result) {
break;
}
for id in results {
if !response.add_id(id) {
break;
}
response.update_results(paginate.build())?;
}
Ok(response)
response.build()
}
}

View File

@@ -4,18 +4,19 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::changes::state::StateManager;
use crate::{api::query::QueryResponseBuilder, changes::state::StateManager};
use common::Server;
use email::sieve::ingest::SieveScriptIngest;
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
method::query::{Filter, QueryRequest, QueryResponse},
object::sieve::{Sieve, SieveComparator, SieveFilter},
};
use std::future::Future;
use store::{
query::{self},
roaring::RoaringBitmap,
IndexKeyPrefix, IterateParams, U32_LEN, ahash::AHashSet, roaring::RoaringBitmap,
search::SearchFilter, write::key::DeserializeBigEndian,
};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
field::SieveField,
@@ -48,73 +49,149 @@ impl SieveScriptQuery for Server {
None
};
let mut document_ids = RoaringBitmap::new();
let mut names = Vec::new();
self.store()
.iterate(
IterateParams::new(
IndexKeyPrefix {
account_id,
collection: Collection::SieveScript.into(),
field: SieveField::Name.into(),
},
IndexKeyPrefix {
account_id,
collection: Collection::SieveScript.into(),
field: u8::from(Collection::SieveScript) + 1,
},
)
.no_values(),
|key, _| {
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
names.push((
document_id,
key.get(IndexKeyPrefix::len()..key.len() - U32_LEN)
.and_then(|v| std::str::from_utf8(v).ok())
.unwrap_or_default()
.to_string(),
));
document_ids.insert(document_id);
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Property(cond) => match cond {
SieveFilter::Name(name) => {
filters.push(SearchFilter::contains(SieveField::Name, &name))
let name = name.to_lowercase();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
names
.iter()
.filter_map(|(id, n)| (n.contains(&name)).then_some(*id))
.collect::<Vec<_>>(),
)));
}
SieveFilter::IsActive(is_active) => {
if !is_active {
filters.push(SearchFilter::Not);
}
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
active_script_id,
)));
if !is_active {
filters.push(SearchFilter::End);
let active_script_id = active_script_id.unwrap();
if is_active {
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter([
active_script_id,
])));
} else {
let mut inactive_set = document_ids.clone();
inactive_set.remove(active_script_id);
filters.push(SearchFilter::is_in_set(inactive_set));
}
}
SieveFilter::_T(other) => {
return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let result_set = self
.filter(account_id, Collection::SieveScript, filters)
.await?;
// Parse sort criteria
let mut sort_by_active = None;
for comparator in request
.sort
.take()
.filter(|s| !s.is_empty())
.unwrap_or_default()
{
match comparator.property {
SieveComparator::Name => {
if !comparator.is_ascending {
names.reverse();
}
}
SieveComparator::IsActive => {
sort_by_active = Some(comparator.is_ascending);
}
SieveComparator::_T(other) => {
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
}
};
}
let (response, paginate) = self
.build_query_response(
result_set.results.len() as usize,
self.get_state(account_id, SyncCollection::SieveScript)
.await?,
&request,
)
.await?;
let mut results = self
.search_store()
.query(account_id, Collection::SieveScript, filters, vec![])
.await?
.into_iter()
.collect::<AHashSet<_>>();
if let Some(paginate) = paginate {
// Parse sort criteria
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
for comparator in request
.sort
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![Comparator::descending(SieveComparator::Name)])
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
self.get_state(account_id, SyncCollection::SieveScript)
.await?,
&request,
);
if !results.is_empty() {
if matches!(sort_by_active, Some(true))
&& results.remove(&active_script_id.unwrap_or_default())
&& !response.add(0, active_script_id.unwrap())
{
comparators.push(match comparator.property {
SieveComparator::Name => {
SearchComparator::field(SieveField::Name, comparator.is_ascending)
}
SieveComparator::IsActive => SearchComparator::set(
RoaringBitmap::from_iter(active_script_id),
comparator.is_ascending,
),
SieveComparator::_T(other) => {
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
}
});
return response.build();
}
// Sort results
self.sort(result_set, comparators, paginate, response).await
} else {
Ok(response)
let mut last_id = None;
for (document_id, _) in names {
if results.contains(&document_id) {
if sort_by_active.is_some() && Some(document_id) == active_script_id {
last_id = Some(document_id);
} else if !response.add(0, document_id) {
return response.build();
}
}
}
if let Some(active_id) = last_id {
response.add(0, active_id);
}
}
response.build()
}
}

View File

@@ -404,13 +404,13 @@ impl SieveScriptSet for Server {
.as_ref()
.is_none_or(|(_, obj)| obj.inner.name != value.as_ref())
&& let Some(id) = self
.filter(
.document_ids_matching(
ctx.resource_token.account_id,
Collection::SieveScript,
vec![Filter::eq(SieveField::Name, value.as_bytes().to_vec())],
SieveField::Name,
value.as_bytes(),
)
.await?
.results
.min()
{
return Ok(Err(SetError::already_exists()

View File

@@ -19,7 +19,11 @@ use jmap_tools::{Key, Map, Value};
use smtp::queue::{ArchivedError, ArchivedErrorDetails, ArchivedStatus, Message, spool::SmtpSpool};
use smtp_proto::ArchivedResponse;
use std::future::Future;
use store::rkyv::option::ArchivedOption;
use store::{
IterateParams, U32_LEN, ValueKey,
rkyv::option::ArchivedOption,
write::{IndexPropertyClass, ValueClass, key::DeserializeBigEndian, now},
};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
@@ -54,21 +58,45 @@ impl EmailSubmissionGet for Server {
EmailSubmissionProperty::MdnBlobIds,
]);
let account_id = request.account_id.document_id();
let email_submission_ids = self
.document_ids(
account_id,
Collection::EmailSubmission,
EmailSubmissionField::EmailId,
)
.await?;
let ids = if let Some(ids) = ids {
ids
} else {
email_submission_ids
.iter()
.take(self.core.jmap.get_max_objects)
.map(Into::into)
.collect::<Vec<_>>()
let mut ids = Vec::with_capacity(16);
self.store()
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: EmailSubmissionField::Metadata.into(),
value: now() - (3 * 86400),
}),
},
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: EmailSubmissionField::Metadata.into(),
value: u64::MAX,
}),
},
)
.ascending()
.no_values(),
|key, _| {
ids.push(Id::from(key.deserialize_be_u32(key.len() - U32_LEN)?));
Ok(ids.len() < self.core.jmap.get_max_objects)
},
)
.await
.caused_by(trc::location!())?;
ids
};
let mut response = GetResponse {
account_id: request.account_id.into(),
@@ -83,10 +111,6 @@ impl EmailSubmissionGet for Server {
for id in ids {
// Obtain the email_submission object
let document_id = id.document_id();
if !email_submission_ids.contains(document_id) {
response.not_found.push(id);
continue;
}
let submission_ = if let Some(submission) = self
.archive(account_id, Collection::EmailSubmission, document_id)
.await?

View File

@@ -4,19 +4,23 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::changes::state::StateManager;
use crate::{api::query::QueryResponseBuilder, changes::state::StateManager};
use common::Server;
use email::submission::UndoStatus;
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse},
method::query::{Filter, QueryRequest, QueryResponse},
object::email_submission::{self, EmailSubmissionComparator, EmailSubmissionFilter},
request::IntoValid,
};
use std::future::Future;
use store::{
SerializeInfallible,
query::{self},
IterateParams, U32_LEN, U64_LEN, ValueKey,
ahash::AHashSet,
roaring::RoaringBitmap,
search::SearchFilter,
write::{IndexPropertyClass, ValueClass, key::DeserializeBigEndian, now},
};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
field::EmailSubmissionField,
@@ -29,123 +33,213 @@ pub trait EmailSubmissionQuery: Sync + Send {
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
}
struct Submission {
document_id: u32,
send_at: u64,
email_id: u32,
thread_id: u32,
identity_id: u32,
undo_status: u8,
}
impl EmailSubmissionQuery for Server {
async fn email_submission_query(
&self,
mut request: QueryRequest<email_submission::EmailSubmission>,
) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id();
let mut filters = Vec::with_capacity(request.filter.len());
let mut submissions = Vec::with_capacity(16);
self.store()
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: EmailSubmissionField::Metadata.into(),
value: now() - (3 * 86400),
}),
},
ValueKey {
account_id,
collection: Collection::CalendarEventNotification.into(),
document_id: 0,
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
property: EmailSubmissionField::Metadata.into(),
value: u64::MAX,
}),
},
)
.ascending(),
|key, value| {
submissions.push(Submission {
document_id: key.deserialize_be_u32(key.len() - U32_LEN)?,
send_at: key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?,
email_id: value.deserialize_be_u32(0)?,
thread_id: value.deserialize_be_u32(U32_LEN)?,
identity_id: value.deserialize_be_u32(U32_LEN + U32_LEN)?,
undo_status: value.last().copied().unwrap(),
});
Ok(true)
},
)
.await
.caused_by(trc::location!())?;
let mut filters = Vec::with_capacity(request.filter.len());
for cond in std::mem::take(&mut request.filter) {
match cond {
Filter::Property(cond) => match cond {
EmailSubmissionFilter::IdentityIds(ids) => {
filters.push(SearchFilter::Or);
for id in ids.into_valid() {
filters.push(SearchFilter::eq(
EmailSubmissionField::IdentityId,
id.document_id().serialize(),
));
}
filters.push(SearchFilter::End);
let ids = ids
.into_valid()
.map(|id| id.document_id())
.collect::<AHashSet<_>>();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
submissions
.iter()
.filter(|s| ids.contains(&s.identity_id))
.map(|s| s.document_id),
)));
}
EmailSubmissionFilter::EmailIds(ids) => {
filters.push(SearchFilter::Or);
for id in ids.into_valid() {
filters.push(SearchFilter::eq(
EmailSubmissionField::EmailId,
id.id().serialize(),
));
}
filters.push(SearchFilter::End);
let ids = ids
.into_valid()
.map(|id| id.document_id())
.collect::<AHashSet<_>>();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
submissions
.iter()
.filter(|s| ids.contains(&s.email_id))
.map(|s| s.document_id),
)));
}
EmailSubmissionFilter::ThreadIds(ids) => {
filters.push(SearchFilter::Or);
for id in ids.into_valid() {
filters.push(SearchFilter::eq(
EmailSubmissionField::ThreadId,
id.document_id().serialize(),
));
}
filters.push(SearchFilter::End);
let ids = ids
.into_valid()
.map(|id| id.document_id())
.collect::<AHashSet<_>>();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
submissions
.iter()
.filter(|s| ids.contains(&s.thread_id))
.map(|s| s.document_id),
)));
}
EmailSubmissionFilter::UndoStatus(undo_status) => {
filters.push(SearchFilter::eq(
EmailSubmissionField::UndoStatus,
match undo_status {
email_submission::UndoStatus::Pending => UndoStatus::Pending,
email_submission::UndoStatus::Final => UndoStatus::Final,
email_submission::UndoStatus::Canceled => UndoStatus::Canceled,
}
.as_index()
.serialize(),
))
let undo_status = match undo_status {
email_submission::UndoStatus::Pending => UndoStatus::Pending,
email_submission::UndoStatus::Final => UndoStatus::Final,
email_submission::UndoStatus::Canceled => UndoStatus::Canceled,
}
.as_index();
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
submissions
.iter()
.filter(|s| s.undo_status == undo_status)
.map(|s| s.document_id),
)));
}
EmailSubmissionFilter::Before(before) => {
let before = before.timestamp() as u64;
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
submissions
.iter()
.filter(|s| s.send_at < before)
.map(|s| s.document_id),
)));
}
EmailSubmissionFilter::After(after) => {
let after = after.timestamp() as u64;
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
submissions
.iter()
.filter(|s| s.send_at > after)
.map(|s| s.document_id),
)));
}
EmailSubmissionFilter::Before(before) => filters.push(SearchFilter::lt(
EmailSubmissionField::SendAt,
(before.timestamp() as u64).serialize(),
)),
EmailSubmissionFilter::After(after) => filters.push(SearchFilter::gt(
EmailSubmissionField::SendAt,
(after.timestamp() as u64).serialize(),
)),
EmailSubmissionFilter::_T(other) => {
return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other));
}
},
Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into());
Filter::And => {
filters.push(SearchFilter::And);
}
Filter::Or => {
filters.push(SearchFilter::Or);
}
Filter::Not => {
filters.push(SearchFilter::Not);
}
Filter::Close => {
filters.push(SearchFilter::End);
}
}
}
let result_set = self
.filter(account_id, Collection::EmailSubmission, filters)
.await?;
let results = self
.search_store()
.query(account_id, Collection::ContactCard, filters, vec![])
.await?
.into_iter()
.collect::<AHashSet<_>>();
let (response, paginate) = self
.build_query_response(
result_set.results.len() as usize,
self.get_state(account_id, SyncCollection::EmailSubmission)
.await?,
&request,
)
.await?;
let mut response = QueryResponseBuilder::new(
results.len(),
self.core.jmap.query_max_results,
self.get_state(account_id, SyncCollection::EmailSubmission)
.await?,
&request,
);
if let Some(paginate) = paginate {
// Parse sort criteria
let mut comparators = Vec::with_capacity(request.sort.as_ref().map_or(1, |s| s.len()));
for comparator in request
.sort
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![Comparator::descending(EmailSubmissionComparator::SentAt)])
{
comparators.push(match comparator.property {
EmailSubmissionComparator::EmailId => SearchComparator::field(
EmailSubmissionField::EmailId,
comparator.is_ascending,
),
EmailSubmissionComparator::ThreadId => SearchComparator::field(
EmailSubmissionField::ThreadId,
comparator.is_ascending,
),
EmailSubmissionComparator::SentAt => SearchComparator::field(
EmailSubmissionField::SendAt,
comparator.is_ascending,
),
if !results.is_empty() {
if let Some(comparator) = request.sort.take().unwrap_or_default().into_iter().next() {
match comparator.property {
EmailSubmissionComparator::EmailId => {
if comparator.is_ascending {
submissions.sort_by_key(|s| s.email_id);
} else {
submissions.sort_by_key(|s| u32::MAX - s.email_id);
}
}
EmailSubmissionComparator::ThreadId => {
if comparator.is_ascending {
submissions.sort_by_key(|s| s.thread_id);
} else {
submissions.sort_by_key(|s| u32::MAX - s.thread_id);
}
}
EmailSubmissionComparator::SentAt => {
if !comparator.is_ascending {
submissions.reverse();
}
}
EmailSubmissionComparator::_T(other) => {
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
}
});
}
}
// Sort results
self.sort(result_set, comparators, paginate, response).await
} else {
Ok(response)
for submission in submissions {
if results.contains(&submission.document_id)
&& !response.add(0, submission.document_id)
{
break;
}
}
}
response.build()
}
}

View File

@@ -14,17 +14,9 @@ use jmap_proto::{
};
use jmap_tools::Map;
use std::future::Future;
use store::{
ahash::AHashMap,
query::{Comparator, ResultSet, sort::Pagination},
roaring::RoaringBitmap,
};
use store::{ahash::AHashMap, roaring::RoaringBitmap};
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
field::EmailField,
id::Id,
};
use types::{collection::SyncCollection, id::Id};
pub trait ThreadGet: Sync + Send {
fn thread_get(
@@ -83,23 +75,11 @@ impl ThreadGet for Server {
let mut thread: Map<'_, ThreadProperty, ThreadValue> =
Map::with_capacity(2).with_key_value(ThreadProperty::Id, id);
if add_email_ids {
let doc_count = document_ids.len() as usize;
let todo = " sorted as vec![Comparator::ascending(EmailField::ReceivedAt)],";
thread.insert_unchecked(
ThreadProperty::EmailIds,
self.core
.storage
.data
.sort(
ResultSet::new(account_id, Collection::Email, document_ids),
vec![],
Pagination::new(doc_count, 0, None, 0),
)
.await
.caused_by(trc::location!())?
.ids
document_ids
.into_iter()
.map(|id| Id::from_parts(thread_id, id.document_id()))
.map(|id| Id::from_parts(thread_id, id))
.collect::<Vec<_>>(),
);
}

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{ changes::state::StateManager};
use crate::changes::state::StateManager;
use common::Server;
use email::sieve::{SieveScript, ingest::SieveScriptIngest};
use jmap_proto::{
@@ -17,7 +17,6 @@ use jmap_proto::{
};
use jmap_tools::{Map, Value};
use std::future::Future;
use store::SearchFilter;
use trc::AddContext;
use types::{
collection::{Collection, SyncCollection},
@@ -162,12 +161,13 @@ impl VacationResponseGet for Server {
}
async fn get_vacation_sieve_script_id(&self, account_id: u32) -> trc::Result<Option<u32>> {
self.filter(
self.document_ids_matching(
account_id,
Collection::SieveScript,
vec![Filter::eq(SieveField::Name, "vacation".as_bytes().to_vec())],
SieveField::Name,
"vacation".as_bytes(),
)
.await
.map(|r| r.results.min())
.map(|r| r.min())
}
}