diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/ActiveEntityModifier.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/ActiveEntityModifier.java deleted file mode 100644 index 534a7b3f..00000000 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/ActiveEntityModifier.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. - */ -package com.sap.cds.feature.attachments.handler.draftservice; - -import com.sap.cds.ql.CQL; -import com.sap.cds.ql.RefBuilder; -import com.sap.cds.ql.RefBuilder.RefSegment; -import com.sap.cds.ql.StructuredTypeRef; -import com.sap.cds.ql.Value; -import com.sap.cds.ql.cqn.CqnComparisonPredicate.Operator; -import com.sap.cds.ql.cqn.CqnPredicate; -import com.sap.cds.ql.cqn.CqnStructuredTypeRef; -import com.sap.cds.ql.cqn.Modifier; -import com.sap.cds.services.draft.Drafts; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The class is used to modify the following values in a given {@link CqnStructuredTypeRef}: - * - * - */ -class ActiveEntityModifier implements Modifier { - - private static final Logger logger = LoggerFactory.getLogger(ActiveEntityModifier.class); - - private final boolean isActiveEntity; - private final String fullEntityName; - - ActiveEntityModifier(boolean isActiveEntity, String fullEntityName) { - this.isActiveEntity = isActiveEntity; - this.fullEntityName = fullEntityName; - } - - @Override - public CqnStructuredTypeRef ref(CqnStructuredTypeRef original) { - RefBuilder ref = CQL.copy(original); - RefSegment rootSegment = ref.rootSegment(); - logger.debug( - "Modifying ref {} with isActiveEntity: {} and fullEntityName: {}", - rootSegment, - isActiveEntity, - fullEntityName); - rootSegment.id(fullEntityName); - - Modifier modifier = new ActiveEntityModifier(isActiveEntity, fullEntityName); - for (RefSegment segment : ref.segments()) { - segment.filter(segment.filter().map(filter -> CQL.copy(filter, modifier)).orElse(null)); - } - return ref.build(); - } - - @Override - public CqnPredicate comparison(Value lhs, Operator op, Value rhs) { - Value rhsNew = rhs; - Value lhsNew = lhs; - if (lhs.isRef() && Drafts.IS_ACTIVE_ENTITY.equals(lhs.asRef().lastSegment())) { - rhsNew = CQL.constant(isActiveEntity); - } - if (rhs.isRef() && Drafts.IS_ACTIVE_ENTITY.equals(rhs.asRef().lastSegment())) { - lhsNew = CQL.constant(isActiveEntity); - } - return CQL.comparison(lhsNew, op, rhsNew); - } -} diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandler.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandler.java index 3faff370..35498859 100644 --- a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandler.java +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandler.java @@ -58,7 +58,10 @@ public DraftCancelAttachmentsHandler( @Before @HandlerOrder(HandlerOrder.LATE) void processBeforeDraftCancel(DraftCancelEventContext context) { - if (isWhereEmpty(context)) { + // We only process the draft cancel event if there is no WHERE clause in the CQN + // and if the target entity is an attachment entity or has attachment associations. + if ((isAttachmentEntity(context.getTarget()) || hasAttachmentAssociations(context.getTarget())) + && isWhereEmpty(context)) { logger.debug( "Processing before {} event for entity {}", context.getEvent(), context.getTarget()); @@ -98,17 +101,49 @@ private Validator buildDeleteContentValidator( }; } + // This function checks if the WHERE clause of the CQN is empty. + // This is the current way to verify that we are really cancelling a draft and not doing sth else. + // Also see here: + // https://github.com/cap-java/cds-feature-attachments/blob/main/doc/Design.md#events + // Unfortunately, context.getEvent() does not return a reliable value in this case. private boolean isWhereEmpty(DraftCancelEventContext context) { return context.getCqn().where().isEmpty(); } + // This function checks if the given entity is of type Attachments + private boolean isAttachmentEntity(CdsEntity entity) { + boolean hasAttachmentInName = entity.getQualifiedName().toLowerCase().contains("attachment"); + + boolean hasFileNameElement = + entity.elements().anyMatch(element -> Attachments.FILE_NAME.equals(element.getName())); + + logger.debug( + "Entity: {}, hasAttachmentInName: {}, hasFileNameElement: {}", + entity.getQualifiedName(), + hasAttachmentInName, + hasFileNameElement); + + return hasAttachmentInName || hasFileNameElement; + } + + // This function checks if the given entity has attachment associations. + private boolean hasAttachmentAssociations(CdsEntity entity) { + return entity + .elements() + .anyMatch(element -> element.getName().toLowerCase().contains("attachment")); + } + private List readAttachments( DraftCancelEventContext context, CdsStructuredType entity, boolean isActiveEntity) { - CqnDelete cqnInactiveEntity = + logger.debug( + "Reading attachments for entity {} (isActiveEntity={})", entity.getName(), isActiveEntity); + logger.debug("Original CQN: {}", context.getCqn()); + CqnDelete modifiedCQN = CQL.copy( - context.getCqn(), new ActiveEntityModifier(isActiveEntity, entity.getQualifiedName())); - return attachmentsReader.readAttachments( - context.getModel(), (CdsEntity) entity, cqnInactiveEntity); + context.getCqn(), + new ModifierToCreateFlatCQN(isActiveEntity, entity.getQualifiedName())); + logger.debug("Modified CQN: {}", modifiedCQN); + return attachmentsReader.readAttachments(context.getModel(), (CdsEntity) entity, modifiedCQN); } private List getCondensedActiveAttachments( diff --git a/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/ModifierToCreateFlatCQN.java b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/ModifierToCreateFlatCQN.java new file mode 100644 index 00000000..9cf1c8c1 --- /dev/null +++ b/cds-feature-attachments/src/main/java/com/sap/cds/feature/attachments/handler/draftservice/ModifierToCreateFlatCQN.java @@ -0,0 +1,91 @@ +/* + * © 2024-2025 SAP SE or an SAP affiliate company and cds-feature-attachments contributors. + */ +package com.sap.cds.feature.attachments.handler.draftservice; + +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.RefBuilder; +import com.sap.cds.ql.RefBuilder.RefSegment; +import com.sap.cds.ql.StructuredTypeRef; +import com.sap.cds.ql.Value; +import com.sap.cds.ql.cqn.CqnComparisonPredicate.Operator; +import com.sap.cds.ql.cqn.CqnPredicate; +import com.sap.cds.ql.cqn.CqnStructuredTypeRef; +import com.sap.cds.ql.cqn.Modifier; +import com.sap.cds.services.draft.Drafts; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A CQL modifier that transforms entity references for draft/active entity handling. + * + *

This modifier flattens complex entity references by removing nested references and creating a + * new CQN statement for the specified {@code fullEntityName}. It performs the following + * transformations: + * + *

    + *
  • Removes nested references and creates a new entity reference for {@code fullEntityName} + *
  • Preserves the filter from the last segment of the original {@link CqnStructuredTypeRef} + *
  • Adds an {@code IsActiveEntity} filter with the specified boolean value + *
+ * + *

This is primarily used in draft service scenarios to transform queries between draft entities + * (IsActiveEntity = false) and active entities (IsActiveEntity = true). + */ +class ModifierToCreateFlatCQN implements Modifier { + + private static final Logger logger = LoggerFactory.getLogger(ModifierToCreateFlatCQN.class); + + private final boolean isActiveEntity; + private final String fullEntityName; + + ModifierToCreateFlatCQN(boolean isActiveEntity, String fullEntityName) { + this.isActiveEntity = isActiveEntity; + this.fullEntityName = fullEntityName; + } + + @Override + public CqnStructuredTypeRef ref(CqnStructuredTypeRef original) { + RefBuilder ref = CQL.copy(original); + RefSegment rootSegment = ref.rootSegment(); + logger.debug( + "Modifying ref {} with isActiveEntity: {} and fullEntityName: {}", + rootSegment, + isActiveEntity, + fullEntityName); + + // Get the filter from the last segment: + // Get the last segment with targetSegment, then an Optional with filter() + // which is then unwrapped to CqnPredicate or null by orElse(null). + CqnPredicate lastSegmentFilter = original.targetSegment().filter().orElse(null); + + // Create an IsActiveEntity filter + CqnPredicate isActiveEntityFilter = CQL.get(Drafts.IS_ACTIVE_ENTITY).eq(isActiveEntity); + + // Combine with original filter if it exists + CqnPredicate combinedFilter = + lastSegmentFilter != null + ? CQL.and(lastSegmentFilter, isActiveEntityFilter) + : isActiveEntityFilter; + + // Apply any additional modifications (like replacing other IsActiveEntity references) + // This calls the comparison() method below for each comparison in the filter + CqnPredicate modifiedFilter = CQL.copy(combinedFilter, this); + + // Create a new entity reference with the modified filter + return CQL.entity(fullEntityName).filter(modifiedFilter).asRef(); + } + + @Override + public CqnPredicate comparison(Value lhs, Operator op, Value rhs) { + Value rhsNew = rhs; + Value lhsNew = lhs; + if (lhs.isRef() && Drafts.IS_ACTIVE_ENTITY.equals(lhs.asRef().lastSegment())) { + rhsNew = CQL.constant(isActiveEntity); + } + if (rhs.isRef() && Drafts.IS_ACTIVE_ENTITY.equals(rhs.asRef().lastSegment())) { + lhsNew = CQL.constant(isActiveEntity); + } + return CQL.comparison(lhsNew, op, rhsNew); + } +} diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java index 6ff55dd1..b0229cd5 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/configuration/RegistrationTest.java @@ -110,7 +110,30 @@ void handlersAreRegistered() { var handlerSize = 8; verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); - var handlers = handlerArgumentCaptor.getAllValues(); + checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize); + } + + @Test + void handlersAreRegisteredWithoutOutboxService() { + when(serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME)) + .thenReturn(persistenceService); + when(serviceCatalog.getService(AttachmentService.class, AttachmentService.DEFAULT_NAME)) + .thenReturn(attachmentService); + when(serviceCatalog.getServices(DraftService.class)).thenReturn(Stream.of(draftService)); + when(serviceCatalog.getServices(ApplicationService.class)) + .thenReturn(Stream.of(applicationService)); + // Return null for OutboxService to test the missing branch + when(serviceCatalog.getService(OutboxService.class, OutboxService.PERSISTENT_UNORDERED_NAME)) + .thenReturn(null); + + cut.eventHandlers(configurer); + + var handlerSize = 8; + verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); + checkHandlers(handlerArgumentCaptor.getAllValues(), handlerSize); + } + + private void checkHandlers(List handlers, int handlerSize) { assertThat(handlers).hasSize(handlerSize); isHandlerForClassIncluded(handlers, DefaultAttachmentsServiceHandler.class); isHandlerForClassIncluded(handlers, CreateAttachmentsHandler.class); diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandlerTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandlerTest.java index 5efbd1be..f96138c4 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandlerTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/DraftCancelAttachmentsHandlerTest.java @@ -6,10 +6,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; import com.sap.cds.feature.attachments.generated.test.cds4j.unit.test.testservice.Attachment; @@ -70,6 +67,24 @@ void whereConditionIncludedNothingHappens() { verifyNoInteractions(attachmentsReader, deleteContentAttachmentEvent); } + @Test + void entityHasNoAttachmentsAndIsNotAttachmentEntityNothingHappens() { + // Test the case where isAttachmentEntity and hasAttachmentAssociations both return false + CdsEntity mockEntity = mock(CdsEntity.class); + // Entity has no elements with name "attachment" + when(mockEntity.getQualifiedName()) + .thenReturn("TestService.RegularEntity"); // No "Attachment" in name + when(eventContext.getTarget()).thenReturn(mockEntity); + + CqnDelete mockDelete = mock(CqnDelete.class); + when(mockDelete.where()).thenReturn(Optional.empty()); + when(eventContext.getCqn()).thenReturn(mockDelete); + + cut.processBeforeDraftCancel(eventContext); + + verifyNoInteractions(attachmentsReader); + } + @Test void nothingSelectedNothingToDo() { getEntityAndMockContext(RootTable_.CDS_NAME); @@ -84,6 +99,33 @@ void nothingSelectedNothingToDo() { @Test void attachmentReaderCorrectCalled() { + getEntityAndMockContext(Attachment_.CDS_NAME); + CqnDelete delete = Delete.from(Attachment_.class); + when(eventContext.getCqn()).thenReturn(delete); + when(eventContext.getModel()).thenReturn(runtime.getCdsModel()); + + cut.processBeforeDraftCancel(eventContext); + + CdsEntity target = eventContext.getTarget(); + verify(attachmentsReader) + .readAttachments(eq(runtime.getCdsModel()), eq(target), deleteArgumentCaptor.capture()); + // Check if the modified CqnDelete that is passed to readAttachments looks correct + CqnDelete modifiedCQN = deleteArgumentCaptor.getValue(); + assertThat(modifiedCQN.toJson()) + .isEqualTo( + "{\"DELETE\":{\"from\":{\"ref\":[{\"id\":\"unit.test.TestService.Attachment\",\"where\":[{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}]}}}"); + + deleteArgumentCaptor = ArgumentCaptor.forClass(CqnDelete.class); + CdsEntity siblingTarget = target.getTargetOf(Drafts.SIBLING_ENTITY); + verify(attachmentsReader) + .readAttachments( + eq(runtime.getCdsModel()), eq(siblingTarget), deleteArgumentCaptor.capture()); + CqnDelete siblingDelete = deleteArgumentCaptor.getValue(); + assertThat(siblingDelete.toJson()).isNotEqualTo(delete.toJson()); + } + + @Test + void attachmentReaderCorrectCalledForEntityWithAttachmentAssociations() { getEntityAndMockContext(RootTable_.CDS_NAME); CqnDelete delete = Delete.from(RootTable_.class); when(eventContext.getCqn()).thenReturn(delete); @@ -94,8 +136,11 @@ void attachmentReaderCorrectCalled() { CdsEntity target = eventContext.getTarget(); verify(attachmentsReader) .readAttachments(eq(runtime.getCdsModel()), eq(target), deleteArgumentCaptor.capture()); - CqnDelete originDelete = deleteArgumentCaptor.getValue(); - assertThat(originDelete.toJson()).isEqualTo(delete.toJson()); + // Check if the modified CqnDelete that is passed to readAttachments looks correct + CqnDelete modifiedCQN = deleteArgumentCaptor.getValue(); + assertThat(modifiedCQN.toJson()) + .isEqualTo( + "{\"DELETE\":{\"from\":{\"ref\":[{\"id\":\"unit.test.TestService.RootTable\",\"where\":[{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}]}}}"); deleteArgumentCaptor = ArgumentCaptor.forClass(CqnDelete.class); CdsEntity siblingTarget = target.getTargetOf(Drafts.SIBLING_ENTITY); @@ -108,8 +153,8 @@ void attachmentReaderCorrectCalled() { @Test void modifierCalledWithCorrectEntitiesIfDraftIsInContext() { - getEntityAndMockContext(RootTable_.CDS_NAME + DraftUtils.DRAFT_TABLE_POSTFIX); - CqnDelete delete = Delete.from(RootTable_.class); + getEntityAndMockContext(Attachment_.CDS_NAME + DraftUtils.DRAFT_TABLE_POSTFIX); + CqnDelete delete = Delete.from(Attachment_.class); when(eventContext.getCqn()).thenReturn(delete); when(eventContext.getModel()).thenReturn(runtime.getCdsModel()); @@ -123,7 +168,9 @@ void modifierCalledWithCorrectEntitiesIfDraftIsInContext() { .readAttachments( eq(runtime.getCdsModel()), eq(siblingTarget), deleteArgumentCaptor.capture()); CqnDelete siblingDelete = deleteArgumentCaptor.getValue(); - assertThat(siblingDelete.toJson()).isEqualTo(delete.toJson()); + assertThat(siblingDelete.toJson()) + .isEqualTo( + "{\"DELETE\":{\"from\":{\"ref\":[{\"id\":\"unit.test.TestService.Attachment\",\"where\":[{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}]}}}"); } @Test diff --git a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/ActiveEntityModifierTest.java b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/ModifierToCreateFlatCQNTest.java similarity index 57% rename from cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/ActiveEntityModifierTest.java rename to cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/ModifierToCreateFlatCQNTest.java index 9024bf01..26d0541d 100644 --- a/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/ActiveEntityModifierTest.java +++ b/cds-feature-attachments/src/test/java/com/sap/cds/feature/attachments/handler/draftservice/ModifierToCreateFlatCQNTest.java @@ -12,7 +12,7 @@ import com.sap.cds.ql.cqn.CqnSelect; import org.junit.jupiter.api.Test; -class ActiveEntityModifierTest { +class ModifierToCreateFlatCQNTest { private static final String TEST_DRAFT_SERVICE_BOOKS = "test.DraftService.Books"; @@ -20,7 +20,7 @@ class ActiveEntityModifierTest { void activeEntityReplacedToFalse() { var select = Select.from(RootTable_.class).where(root -> root.IsActiveEntity().eq(true)); - var result = CQL.copy(select, new ActiveEntityModifier(false, RootTable_.CDS_NAME)); + var result = CQL.copy(select, new ModifierToCreateFlatCQN(false, RootTable_.CDS_NAME)); assertThat(result.toString()) .contains("{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":false}]}"); @@ -31,28 +31,36 @@ void activeEntityReplacedToFalse() { void activeEntityReplacedToTrue() { var select = Select.from(RootTable_.class).where(root -> root.IsActiveEntity().eq(false)); - var result = CQL.copy(select, new ActiveEntityModifier(true, RootTable_.CDS_NAME)); + var result = CQL.copy(select, new ModifierToCreateFlatCQN(true, RootTable_.CDS_NAME)); assertThat(result.toString()).contains("{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}"); assertThat(result.toString()).doesNotContain("false"); } @Test - void entityNameReplaced() { + void entityNameReplacedAndActiveEntity() { var select = Select.from(RootTable_.class).where(root -> root.IsActiveEntity().eq(true)); - var result = CQL.copy(select, new ActiveEntityModifier(true, RootTable_.CDS_NAME + "_draft")); + var result = + CQL.copy(select, new ModifierToCreateFlatCQN(true, RootTable_.CDS_NAME + "_draft")); - assertThat(result.toString()).contains("{\"ref\":[\"unit.test.TestService.RootTable_draft\"]}"); + // Expects the entity to have an IsActiveEntity filter in the reference + assertThat(result.toString()) + .contains( + "{\"id\":\"unit.test.TestService.RootTable_draft\",\"where\":[{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}"); } @Test - void nothingReplaced() { + void entityNameNotReplacedAndActiveEntity() { var select = Select.from(RootTable_.class).where(root -> root.IsActiveEntity().eq(true)); - var result = CQL.copy(select, new ActiveEntityModifier(true, RootTable_.CDS_NAME)); + var result = CQL.copy(select, new ModifierToCreateFlatCQN(true, RootTable_.CDS_NAME)); - assertThat(result).hasToString(select.toString()); + // Expects the entity to have an IsActiveEntity filter in the reference even when entity name + // doesn't change + assertThat(result.toString()) + .contains( + "{\"id\":\"unit.test.TestService.RootTable\",\"where\":[{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}"); } @Test @@ -66,7 +74,7 @@ void selectWithFilterReplace() { .filter(e -> e.get(IS_ACTIVE_ENTITY).eq(false)) .to("relatedBook")); - var result = CQL.copy(select, new ActiveEntityModifier(true, TEST_DRAFT_SERVICE_BOOKS)); + var result = CQL.copy(select, new ModifierToCreateFlatCQN(true, TEST_DRAFT_SERVICE_BOOKS)); assertThat(result.toString()).contains("{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":true}]}"); assertThat(result.toString()).doesNotContain("false"); @@ -88,11 +96,34 @@ void onlyRefActiveEntityIsReplaced() { .eq(root.IsActiveEntity()) .and(CQL.constant(true).eq(root.HasActiveEntity()))))); - var result = CQL.copy(select, new ActiveEntityModifier(false, RootTable_.CDS_NAME)); + var result = CQL.copy(select, new ModifierToCreateFlatCQN(false, RootTable_.CDS_NAME)); assertThat(result.toString()).contains("{\"ref\":[\"IsActiveEntity\"]},\"=\",{\"val\":false}"); assertThat(result.toString()).contains("{\"ref\":[\"HasActiveEntity\"]},\"=\",{\"val\":true}"); assertThat(result.toString()).contains("{\"val\":false},\"=\",{\"ref\":[\"IsActiveEntity\"]}"); assertThat(result.toString()).contains("{\"val\":true},\"=\",{\"ref\":[\"HasActiveEntity\"]}"); } + + @Test + void combinesNonIsActiveEntityFilterWithIsActiveEntityFilter() { + // Create query with a filter on the last/target segment + CqnSelect original = + Select.from( + CQL.entity(RootTable_.CDS_NAME) + .filter(CQL.get("title").eq("Some Title")) // Filter on entity reference + ); + + ModifierToCreateFlatCQN modifier = new ModifierToCreateFlatCQN(true, RootTable_.CDS_NAME); + + var result = CQL.copy(original, modifier); + + // Should contain both the original title filter and IsActiveEntity filter + assertThat(result.toString()).contains("\"title\""); + assertThat(result.toString()).contains("\"Some Title\""); + assertThat(result.toString()).contains("\"IsActiveEntity\""); + assertThat(result.toString()).contains("\"val\":true"); + + // The key assertion: should contain AND operation combining both filters + assertThat(result.toString()).contains("\"and\""); + } }