diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/DataMovementManager.java b/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/DataMovementManager.java
index 31152d6ef..f024b55bf 100644
--- a/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/DataMovementManager.java
+++ b/marklogic-client-api/src/main/java/com/marklogic/client/datamovement/DataMovementManager.java
@@ -7,6 +7,7 @@
import com.marklogic.client.io.marker.ContentHandle;
import com.marklogic.client.query.*;
+import java.io.Closeable;
import java.util.Iterator;
/**
@@ -33,7 +34,15 @@
* dataMovementManager.release();
*}
*/
-public interface DataMovementManager {
+public interface DataMovementManager extends Closeable {
+
+ /**
+ * @since 8.1.0
+ */
+ default void close() {
+ release();
+ }
+
/** Calls release() on all host-specific DatabaseClient instances (but not on
* the DatabaseClient instance used to create this DataMovementManager
* instance).
diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/AbstractClientTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/AbstractClientTest.java
new file mode 100644
index 000000000..020aa21d7
--- /dev/null
+++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/AbstractClientTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ */
+package com.marklogic.client.test;
+
+import com.marklogic.client.DatabaseClient;
+import com.marklogic.junit5.AbstractMarkLogicTest;
+
+/**
+ * Intended to be the base class for all future client API tests, as it properly prepares the database by deleting
+ * documents from previous test runs that were not created as part of deploying the test app.
+ */
+public abstract class AbstractClientTest extends AbstractMarkLogicTest {
+
+ @Override
+ protected final DatabaseClient getDatabaseClient() {
+ return Common.newServerAdminClient();
+ }
+
+ @Override
+ protected final String getJavascriptForDeletingDocumentsBeforeTestRuns() {
+ return """
+ declareUpdate();
+ cts.uris('', [], cts.notQuery(cts.collectionQuery(['test-data', 'temporal-collection'])))
+ .toArray().forEach(item => xdmp.documentDelete(item))
+ """;
+ }
+}
diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/IncrementalWriteTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/IncrementalWriteTest.java
new file mode 100644
index 000000000..83a81e1c5
--- /dev/null
+++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/datamovement/IncrementalWriteTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
+ */
+package com.marklogic.client.test.datamovement;
+
+import com.marklogic.client.DatabaseClient;
+import com.marklogic.client.datamovement.DataMovementManager;
+import com.marklogic.client.datamovement.WriteBatcher;
+import com.marklogic.client.io.DocumentMetadataHandle;
+import com.marklogic.client.io.StringHandle;
+import com.marklogic.client.test.AbstractClientTest;
+import com.marklogic.client.test.Common;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class IncrementalWriteTest extends AbstractClientTest {
+
+ private static final DocumentMetadataHandle METADATA = new DocumentMetadataHandle()
+ .withCollections("incremental-test")
+ .withPermission("rest-reader", DocumentMetadataHandle.Capability.READ, DocumentMetadataHandle.Capability.UPDATE);
+
+ @Test
+ void test() {
+ AtomicInteger writtenCount = new AtomicInteger();
+
+ try (DatabaseClient client = Common.newClient()) {
+ WriteBatcherTemplate template = new WriteBatcherTemplate(client);
+
+ template.runWriteJob(writeBatcher -> writeBatcher
+ .withThreadCount(1)
+ .withBatchSize(10)
+ .onBatchSuccess(batch -> writtenCount.addAndGet(batch.getItems().length)),
+
+ writeBatcher -> {
+ for (int i = 1; i <= 20; i++) {
+ String uri = "/incremental/test/doc-" + i + ".xml";
+ String content = "" + i + "This is document number " + i + "";
+ writeBatcher.add(uri, METADATA, new StringHandle(content));
+ }
+ }
+ );
+ }
+
+ assertEquals(20, writtenCount.get());
+ }
+
+ // Experimenting with a template that gets rid of some annoying DMSDK boilerplate.
+ private record WriteBatcherTemplate(DatabaseClient databaseClient) {
+
+ public void runWriteJob(Consumer writeBatcherConfigurer, Consumer writeBatcherUser) {
+ try (DataMovementManager dmm = databaseClient.newDataMovementManager()) {
+ WriteBatcher writeBatcher = dmm.newWriteBatcher();
+ writeBatcherConfigurer.accept(writeBatcher);
+
+ dmm.startJob(writeBatcher);
+
+ writeBatcherUser.accept(writeBatcher);
+ writeBatcher.awaitCompletion();
+
+ dmm.stopJob(writeBatcher);
+ }
+ }
+ }
+}
diff --git a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java
index 0afa19e8c..df9682e37 100644
--- a/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java
+++ b/marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java
@@ -18,6 +18,7 @@
import com.marklogic.client.row.RawPlanDefinition;
import com.marklogic.client.row.RowManager;
import com.marklogic.client.row.RowRecord;
+import com.marklogic.client.test.AbstractClientTest;
import com.marklogic.client.test.Common;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -32,7 +33,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
-public abstract class AbstractOpticUpdateTest {
+public abstract class AbstractOpticUpdateTest extends AbstractClientTest {
private final static String XML_PREAMBLE = "\n";
@@ -42,10 +43,6 @@ public abstract class AbstractOpticUpdateTest {
@BeforeEach
public void setup() {
- // Subclasses of this test are expected to only write URIs starting with /acme/ (which is used so that test
- // URIs show up near the top when exploring the database in qconsole), so delete all of them before running the
- // test to ensure a document doesn't already exist.
- Common.deleteUrisWithPattern("/acme/*");
Common.client = Common.newClientBuilder().withUsername("writer-no-default-permissions").build();
rowManager = Common.client.newRowManager().withUpdate(true);
op = rowManager.newPlanBuilder();