This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit d7bd1be50dc857340d79068cd24ed6c07cfb22c9 Author: Quan Tran <[email protected]> AuthorDate: Mon Mar 9 15:17:20 2026 +0700 JAMES-4187 Drop Cassandra email query view look up table By refactoring EmailQueryView::delete(MailboxId mailboxId, MessageId messageId) to take into account receivedAt also. --- .../projections/CassandraEmailQueryView.java | 65 ++++------------------ .../CassandraEmailQueryViewDataDefinition.java | 11 ---- .../table/CassandraEmailQueryViewTable.java | 1 - .../projections/PostgresEmailQueryView.java | 2 +- .../james/jmap/api/projections/EmailQueryView.java | 2 +- .../memory/projections/MemoryEmailQueryView.java | 2 +- .../api/projections/EmailQueryViewContract.java | 12 +++- .../jmap/event/PopulateEmailQueryViewListener.java | 31 +++++++++-- 8 files changed, 49 insertions(+), 77 deletions(-) diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java index 614ccc2748..145dfec8c5 100644 --- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryView.java @@ -24,7 +24,6 @@ import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; -import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.DATE_LOOKUP_TABLE; import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.MAILBOX_ID; import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.MESSAGE_ID; import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.RECEIVED_AT; @@ -55,7 +54,6 @@ import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.Row; import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; -import com.datastax.oss.driver.api.querybuilder.QueryBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -67,13 +65,9 @@ public class CassandraEmailQueryView implements EmailQueryView { private final PreparedStatement listMailboxContentByReceivedAt; private final PreparedStatement listMailboxContentSinceReceivedAt; private final PreparedStatement listMailboxContentBeforeReceivedAt; - private final PreparedStatement insertInLookupTable; private final PreparedStatement insertReceivedAt; - private final PreparedStatement deleteLookupRecord; private final PreparedStatement deleteReceivedAt; - private final PreparedStatement deleteAllLookupRecords; private final PreparedStatement deleteAllReceivedAt; - private final PreparedStatement lookupDate; @Inject public CassandraEmailQueryView(CqlSession session) { @@ -102,12 +96,6 @@ public class CassandraEmailQueryView implements EmailQueryView { .limit(bindMarker(LIMIT_MARKER)) .build()); - insertInLookupTable = session.prepare(insertInto(DATE_LOOKUP_TABLE) - .value(MAILBOX_ID, bindMarker(MAILBOX_ID)) - .value(MESSAGE_ID, bindMarker(MESSAGE_ID)) - .value(RECEIVED_AT, bindMarker(RECEIVED_AT)) - .build()); - insertReceivedAt = session.prepare(insertInto(TABLE_NAME_RECEIVED_AT) .value(MAILBOX_ID, bindMarker(MAILBOX_ID)) .value(MESSAGE_ID, bindMarker(MESSAGE_ID)) @@ -115,29 +103,14 @@ public class CassandraEmailQueryView implements EmailQueryView { .value(THREAD_ID, bindMarker(THREAD_ID)) .build()); - deleteLookupRecord = session.prepare(deleteFrom(DATE_LOOKUP_TABLE) - .whereColumn(MAILBOX_ID).isEqualTo(bindMarker(MAILBOX_ID)) - .whereColumn(MESSAGE_ID).isEqualTo(bindMarker(MESSAGE_ID)) - .build()); - - deleteReceivedAt = session.prepare(QueryBuilder.deleteFrom(TABLE_NAME_RECEIVED_AT) + deleteReceivedAt = session.prepare(deleteFrom(TABLE_NAME_RECEIVED_AT) .whereColumn(MAILBOX_ID).isEqualTo(bindMarker(MAILBOX_ID)) .whereColumn(MESSAGE_ID).isEqualTo(bindMarker(MESSAGE_ID)) .whereColumn(RECEIVED_AT).isEqualTo(bindMarker(RECEIVED_AT)) .build()); - deleteAllLookupRecords = session.prepare(QueryBuilder.deleteFrom(DATE_LOOKUP_TABLE) - .whereColumn(MAILBOX_ID).isEqualTo(bindMarker(MAILBOX_ID)) - .build()); - - deleteAllReceivedAt = session.prepare(QueryBuilder.deleteFrom(TABLE_NAME_RECEIVED_AT) - .whereColumn(MAILBOX_ID).isEqualTo(bindMarker(MAILBOX_ID)) - .build()); - - lookupDate = session.prepare(selectFrom(DATE_LOOKUP_TABLE) - .all() + deleteAllReceivedAt = session.prepare(deleteFrom(TABLE_NAME_RECEIVED_AT) .whereColumn(MAILBOX_ID).isEqualTo(bindMarker(MAILBOX_ID)) - .whereColumn(MESSAGE_ID).isEqualTo(bindMarker(MESSAGE_ID)) .build()); } @@ -188,26 +161,14 @@ public class CassandraEmailQueryView implements EmailQueryView { } @Override - public Mono<Void> delete(MailboxId mailboxId, MessageId messageId) { + public Mono<Void> delete(MailboxId mailboxId, ZonedDateTime receivedAt, MessageId messageId) { CassandraMessageId cassandraMessageId = (CassandraMessageId) messageId; CassandraId cassandraId = (CassandraId) mailboxId; - return executor.executeSingleRow(lookupDate.bind() - .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID) - .setUuid(MESSAGE_ID, cassandraMessageId.get())) - .flatMap(row -> doDelete(cassandraMessageId, cassandraId, row)); - } - - public Mono<? extends Void> doDelete(CassandraMessageId cassandraMessageId, CassandraId cassandraId, Row row) { - Instant receivedAt = row.getInstant(RECEIVED_AT); - return executor.executeVoid(deleteReceivedAt.bind() - .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID) - .setUuid(MESSAGE_ID, cassandraMessageId.get()) - .setInstant(RECEIVED_AT, receivedAt)) - .then(executor.executeVoid(deleteLookupRecord.bind() - .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID) - .setUuid(MESSAGE_ID, cassandraMessageId.get()))); + .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID) + .setUuid(MESSAGE_ID, cassandraMessageId.get()) + .setInstant(RECEIVED_AT, receivedAt.toInstant())); } @Override @@ -215,9 +176,7 @@ public class CassandraEmailQueryView implements EmailQueryView { CassandraId cassandraId = (CassandraId) mailboxId; return executor.executeVoid(deleteAllReceivedAt.bind() - .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID)) - .then(executor.executeVoid(deleteAllLookupRecords.bind() - .setUuid(MAILBOX_ID, ((CassandraId) mailboxId).asUuid()))); + .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID)); } @Override @@ -225,14 +184,10 @@ public class CassandraEmailQueryView implements EmailQueryView { CassandraMessageId cassandraMessageId = (CassandraMessageId) messageId; CassandraId cassandraId = (CassandraId) mailboxId; - return executor.executeVoid(insertInLookupTable.bind() + return executor.executeVoid(insertReceivedAt.bind() .setUuid(MESSAGE_ID, cassandraMessageId.get()) .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID) - .setInstant(RECEIVED_AT, receivedAt.toInstant())) - .then(executor.executeVoid(insertReceivedAt.bind() - .setUuid(MESSAGE_ID, cassandraMessageId.get()) - .set(MAILBOX_ID, cassandraId.asUuid(), TypeCodecs.UUID) - .setInstant(RECEIVED_AT, receivedAt.toInstant()) - .setUuid(THREAD_ID, ((CassandraMessageId) threadId.getBaseMessageId()).get()))); + .setInstant(RECEIVED_AT, receivedAt.toInstant()) + .setUuid(THREAD_ID, ((CassandraMessageId) threadId.getBaseMessageId()).get())); } } diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewDataDefinition.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewDataDefinition.java index 56fed840fd..9d7c31f8f8 100644 --- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewDataDefinition.java +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraEmailQueryViewDataDefinition.java @@ -22,7 +22,6 @@ package org.apache.james.jmap.cassandra.projections; import static com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder.DESC; import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.RowsPerPartition.rows; import static org.apache.james.backends.cassandra.utils.CassandraConstants.DEFAULT_CACHED_ROW_PER_PARTITION; -import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.DATE_LOOKUP_TABLE; import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.MAILBOX_ID; import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.MESSAGE_ID; import static org.apache.james.jmap.cassandra.projections.table.CassandraEmailQueryViewTable.RECEIVED_AT; @@ -44,15 +43,5 @@ public interface CassandraEmailQueryViewDataDefinition { .withClusteringColumn(RECEIVED_AT, DataTypes.TIMESTAMP) .withClusteringColumn(MESSAGE_ID, DataTypes.UUID) .withColumn(THREAD_ID, DataTypes.UUID)) - - .table(DATE_LOOKUP_TABLE) - .comment("Given a MailboxId+MessageId lookup the dates of a message to delete it.") - .options(options -> options - .withCaching(true, rows(DEFAULT_CACHED_ROW_PER_PARTITION))) - .statement(statement -> types -> statement - .withPartitionKey(MAILBOX_ID, DataTypes.UUID) - .withClusteringColumn(MESSAGE_ID, DataTypes.UUID) - .withColumn(RECEIVED_AT, DataTypes.TIMESTAMP)) - .build(); } diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/table/CassandraEmailQueryViewTable.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/table/CassandraEmailQueryViewTable.java index 3629752f2d..c1ec984ef0 100644 --- a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/table/CassandraEmailQueryViewTable.java +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/table/CassandraEmailQueryViewTable.java @@ -23,7 +23,6 @@ import com.datastax.oss.driver.api.core.CqlIdentifier; public interface CassandraEmailQueryViewTable { String TABLE_NAME_RECEIVED_AT = "email_query_view_received_at"; - String DATE_LOOKUP_TABLE = "email_query_view_date_lookup"; CqlIdentifier MAILBOX_ID = CqlIdentifier.fromCql("mailboxId"); CqlIdentifier MESSAGE_ID = CqlIdentifier.fromCql("messageId"); diff --git a/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/projections/PostgresEmailQueryView.java b/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/projections/PostgresEmailQueryView.java index 585a4baba8..2701ab72e2 100644 --- a/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/projections/PostgresEmailQueryView.java +++ b/server/data/data-jmap-postgres/src/main/java/org/apache/james/jmap/postgres/projections/PostgresEmailQueryView.java @@ -58,7 +58,7 @@ public class PostgresEmailQueryView implements EmailQueryView { } @Override - public Mono<Void> delete(MailboxId mailboxId, MessageId messageId) { + public Mono<Void> delete(MailboxId mailboxId, ZonedDateTime receivedAt, MessageId messageId) { return emailQueryViewDAO.delete(PostgresMailboxId.class.cast(mailboxId), PostgresMessageId.class.cast(messageId)); } diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java index d0a1720181..148d5fe480 100644 --- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java +++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/EmailQueryView.java @@ -148,7 +148,7 @@ public interface EmailQueryView { */ Flux<MessageId> listMailboxContentBeforeSortedByReceivedAt(MailboxId mailboxId, ZonedDateTime since, Limit limit, boolean collapseThreads); - Mono<Void> delete(MailboxId mailboxId, MessageId messageId); + Mono<Void> delete(MailboxId mailboxId, ZonedDateTime receivedAt, MessageId messageId); Mono<Void> delete(MailboxId mailboxId); diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java index dd649a4345..3571776dcc 100644 --- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java +++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/memory/projections/MemoryEmailQueryView.java @@ -93,7 +93,7 @@ public class MemoryEmailQueryView implements EmailQueryView { } @Override - public Mono<Void> delete(MailboxId mailboxId, MessageId messageId) { + public Mono<Void> delete(MailboxId mailboxId, ZonedDateTime receivedAt, MessageId messageId) { return Mono.fromRunnable(() -> entries.remove(mailboxId, messageId)); } diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java index d7d799daaa..1396f8c446 100644 --- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java +++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/EmailQueryViewContract.java @@ -93,7 +93,17 @@ public interface EmailQueryViewContract { @Test default void deleteShouldNotFailWhenEmpty() { - assertThatCode(() -> testee().delete(mailboxId1(), messageId4()).block()).doesNotThrowAnyException(); + assertThatCode(() -> testee().delete(mailboxId1(), DATE_4, messageId4()).block()).doesNotThrowAnyException(); + } + + @Test + default void deleteShouldRemoveSavedEntry() { + testee().save(mailboxId1(), DATE_4, messageId1(), threadId1()).block(); + + testee().delete(mailboxId1(), DATE_4, messageId1()).block(); + + assertThat(testee().listMailboxContentSortedByReceivedAt(mailboxId1(), Limit.limit(12), !COLLAPSE_THREAD).collectList().block()) + .isEmpty(); } @Test diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java index 31fa0629a6..3408f57dac 100644 --- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java +++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/PopulateEmailQueryViewListener.java @@ -22,6 +22,7 @@ package org.apache.james.jmap.event; import static jakarta.mail.Flags.Flag.DELETED; import static org.apache.james.util.ReactorUtils.publishIfPresent; +import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Optional; @@ -110,8 +111,9 @@ public class PopulateEmailQueryViewListener implements ReactiveGroupEventListene private Publisher<Void> handleExpunged(Expunged expunged) { return Flux.fromStream(expunged.getUids().stream() - .map(uid -> expunged.getMetaData(uid).getMessageId())) - .concatMap(messageId -> viewManager.getEmailQueryView(expunged.getUsername()).delete(expunged.getMailboxId(), messageId)) + .map(expunged::getMetaData)) + .concatMap(metaData -> viewManager.getEmailQueryView(expunged.getUsername()) + .delete(expunged.getMailboxId(), toReceivedAt(metaData.getInternalDate().toInstant()), metaData.getMessageId())) .then(); } @@ -121,9 +123,7 @@ public class PopulateEmailQueryViewListener implements ReactiveGroupEventListene Mono<Void> removeMessagesMarkedAsDeleted = Flux.fromIterable(flagsUpdated.getUpdatedFlags()) .filter(updatedFlags -> updatedFlags.isModifiedToSet(DELETED)) - .map(UpdatedFlags::getMessageId) - .handle(publishIfPresent()) - .concatMap(messageId -> viewManager.getEmailQueryView(flagsUpdated.getUsername()).delete(flagsUpdated.getMailboxId(), messageId)) + .concatMap(updatedFlags -> deleteViewForMessageMarkedAsDeleted(flagsUpdated, session, updatedFlags)) .then(); Mono<Void> addMessagesNoLongerMarkedAsDeleted = Flux.fromIterable(flagsUpdated.getUpdatedFlags()) @@ -171,9 +171,28 @@ public class PopulateEmailQueryViewListener implements ReactiveGroupEventListene } public Mono<Void> handleAdded(MailboxId mailboxId, MessageResult messageResult, Username username) { - ZonedDateTime receivedAt = ZonedDateTime.ofInstant(messageResult.getInternalDate().toInstant(), ZoneOffset.UTC); + ZonedDateTime receivedAt = toReceivedAt(messageResult.getInternalDate().toInstant()); return viewManager.getEmailQueryView(username) .save(mailboxId, receivedAt, messageResult.getMessageId(), messageResult.getThreadId()) .then(); } + + private Mono<Void> deleteViewForMessageMarkedAsDeleted(FlagsUpdated flagsUpdated, MailboxSession session, UpdatedFlags updatedFlags) { + return Mono.justOrEmpty(updatedFlags.getMessageId()) + .flatMap(messageId -> resolveReceivedAt(updatedFlags, messageId, session) + .flatMap(receivedAt -> viewManager.getEmailQueryView(flagsUpdated.getUsername()) + .delete(flagsUpdated.getMailboxId(), receivedAt, messageId))) + .then(); + } + + private Mono<ZonedDateTime> resolveReceivedAt(UpdatedFlags updatedFlags, MessageId messageId, MailboxSession session) { + return Mono.justOrEmpty(updatedFlags.getInternalDate().map(date -> toReceivedAt(date.toInstant()))) + .switchIfEmpty(Flux.from(messageIdManager.getMessagesReactive(ImmutableList.of(messageId), FetchGroup.HEADERS, session)) + .next() + .map(messageResult -> toReceivedAt(messageResult.getInternalDate().toInstant()))); + } + + private ZonedDateTime toReceivedAt(Instant internalDate) { + return ZonedDateTime.ofInstant(internalDate, ZoneOffset.UTC); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
