Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class RowManagerImpl
private RowStructure rowStructureStyle = null;
private Integer optimize;
private String traceLabel;
private boolean update;

public RowManagerImpl(RESTServices services) {
super();
Expand Down Expand Up @@ -124,7 +125,13 @@ public void setOptimize(Integer value) {
this.optimize = value;
}

@Override
@Override
public RowManager withUpdate(boolean update) {
this.update = update;
return this;
}

@Override
public RawPlanDefinition newRawPlanDefinition(JSONWriteHandle handle) {
return new RawPlanDefinitionImpl(handle);
}
Expand Down Expand Up @@ -176,7 +183,11 @@ public <T extends StructureReadHandle> T resultDoc(Plan plan, T resultsHandle, T
.withColumnTypes(getDatatypeStyle())
.withOutput(getRowStructureStyle())
.getRequestParameters();
return services.postResource(requestLogger, "rows", transaction, params, astHandle, resultsHandle);
return services.postResource(requestLogger, determinePath(), transaction, params, astHandle, resultsHandle);
}

private String determinePath() {
return this.update ? "rows/update" : "rows";
}

@Override
Expand Down Expand Up @@ -291,8 +302,9 @@ public <T extends StructureReadHandle> T explain(Plan plan, T resultsHandle) {
RequestParameters params = new RequestParameters();
params.add("output", "explain");

return services.postResource(requestLogger, "rows", null, params, astHandle, resultsHandle);
return services.postResource(requestLogger, determinePath(), null, params, astHandle, resultsHandle);
}

@Override
public <T> T explainAs(Plan plan, Class<T> as) {
ContentHandle<T> handle = handleFor(as);
Expand Down Expand Up @@ -409,11 +421,12 @@ private <T extends AbstractReadHandle> String getRowFormat(T rowHandle) {
private RESTServiceResultIterator submitPlan(PlanBuilderBaseImpl.RequestPlan requestPlan, RequestParameters params, Transaction transaction) {
AbstractWriteHandle astHandle = requestPlan.getHandle();
List<ContentParam> contentParams = requestPlan.getContentParams();
final String path = determinePath();
if (contentParams != null && !contentParams.isEmpty()) {
contentParams.add(new ContentParam(new PlanBuilderBaseImpl.PlanParamBase("query"), astHandle, null));
return services.postMultipartForm(requestLogger, "rows", transaction, params, contentParams);
return services.postMultipartForm(requestLogger, path, transaction, params, contentParams);
}
return services.postIteratedResource(requestLogger, "rows", transaction, params, astHandle);
return services.postIteratedResource(requestLogger, path, transaction, params, astHandle);
}

private PlanBuilderBaseImpl.RequestPlan checkPlan(Plan plan) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ public enum RowStructure{ARRAY, OBJECT}
*/
void setTraceLabel(String label);

/**
* As of MarkLogic 11.2, the "v1/rows/update" endpoint must be used in order to submit an Optic plan that performs
* an update. This method must be called with a value of {@code true} in order for that endpoint to be used instead
* of "v1/rows". You may later call this method with a value of {@code false} in order to submit a plan that does
* not perform an update.
*
* @param update set to {@code true} if submitting a plan that performs an update
* @return the instance of this class
* @since 6.5.0
*/
RowManager withUpdate(boolean update);

/**
* @return the label that will be used for all log messages associated with the "optic" trace event
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void setup() {
.evalAs(String.class);

Common.client = Common.newClientBuilder().withUsername("writer-no-default-permissions").build();
rowManager = Common.client.newRowManager();
rowManager = Common.client.newRowManager().withUpdate(true);
op = rowManager.newPlanBuilder();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void insertDocsWithUserWithDefaultCollectionsAndPermissions() {
mapper.createObjectNode().put("hello", "two")));

Common.client = Common.newClientBuilder().withUsername(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS).build();
Common.client.newRowManager().execute(op
Common.client.newRowManager().withUpdate(true).execute(op
.fromDocDescriptors(op.docDescriptors(writeSet))
.write());

Expand Down Expand Up @@ -88,7 +88,7 @@ public void updateOnlyDocAsUserWithNoDefaults() {
public void updateOnlyDocWithUserWithDefaultCollectionsAndPermissions() {
// Set up client as user with default collections and permissions
Common.client = Common.newClientBuilder().withUsername(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS).build();
rowManager = Common.client.newRowManager();
rowManager = Common.client.newRowManager().withUpdate(true);
op = rowManager.newPlanBuilder();

verifyOnlyDocCanBeUpdatedWithoutLosingAnyMetadata();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.marklogic.client.test.rows;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.marklogic.client.FailedRequestException;
import com.marklogic.client.expression.PlanBuilder;
import com.marklogic.client.io.DocumentMetadataHandle;
import com.marklogic.client.io.JacksonHandle;
Expand All @@ -12,89 +13,99 @@

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(RequiresML11.class)
public class LockForUpdateTest extends AbstractOpticUpdateTest {

@Test
public void basicTest() {
final String uri = "/acme/doc1.json";

// Write a document
rowManager.execute(op.fromDocDescriptors(
op.docDescriptor(newWriteOp(uri, new JacksonHandle(mapper.createObjectNode().put("hello", "world")))))
.write());
verifyJsonDoc(uri, doc -> assertEquals("world", doc.get("hello").asText()));

// Construct a plan that will lock the URI and update its collection
PlanBuilder.ModifyPlan plan = op
.fromDocDescriptors(
op.docDescriptor(newWriteOp(uri, new DocumentMetadataHandle().withCollections("optic1"), null))
)
.lockForUpdate()
.write(op.docCols(null, op.xs.stringSeq("uri", "collections")));

// Run an eval that locks the URI and sleeps for 2 seconds, which will block the plan run below
new Thread(() -> {
Common.newServerAdminClient().newServerEval()
.javascript(String.format("declareUpdate(); " +
"xdmp.lockForUpdate('%s'); " +
"xdmp.sleep(2000); " +
"xdmp.documentSetCollections('%s', ['eval1']);", uri, uri))
.evalAs(String.class);
}).start();

// Immediately run a plan that updates the collections as well; this should be blocked while the eval thread
// above completes
long start = System.currentTimeMillis();
rowManager.execute(plan);
long duration = System.currentTimeMillis() - start;
System.out.println("DUR: " + duration);

assertTrue(duration > 1500,
@Test
public void basicTest() {
final String uri = "/acme/doc1.json";

// Write a document
rowManager.execute(op.fromDocDescriptors(
op.docDescriptor(newWriteOp(uri, new JacksonHandle(mapper.createObjectNode().put("hello", "world")))))
.write());
verifyJsonDoc(uri, doc -> assertEquals("world", doc.get("hello").asText()));

// Construct a plan that will lock the URI and update its collection
PlanBuilder.ModifyPlan plan = op
.fromDocDescriptors(
op.docDescriptor(newWriteOp(uri, new DocumentMetadataHandle().withCollections("optic1"), null))
)
.lockForUpdate()
.write(op.docCols(null, op.xs.stringSeq("uri", "collections")));

// Run an eval that locks the URI and sleeps for 2 seconds, which will block the plan run below
new Thread(() -> {
Common.newServerAdminClient().newServerEval()
.javascript(String.format("declareUpdate(); " +
"xdmp.lockForUpdate('%s'); " +
"xdmp.sleep(2000); " +
"xdmp.documentSetCollections('%s', ['eval1']);", uri, uri))
.evalAs(String.class);
}).start();

// Immediately run a plan that updates the collections as well; this should be blocked while the eval thread
// above completes
long start = System.currentTimeMillis();
rowManager.execute(plan);
long duration = System.currentTimeMillis() - start;
System.out.println("DUR: " + duration);

assertTrue(duration > 1500,
"Because the eval call slept for 2 seconds, the duration of the plan execution should be at least " +
"1500ms, which is much longer than normal; it may not be at least 2 seconds due to the small delay in " +
"the Java layer of executing the plan; duration: " + duration);

// Verify that the collections were set based on the plan, which should have run second
verifyMetadata(uri, metadata -> {
DocumentMetadataHandle.DocumentCollections colls = metadata.getCollections();
assertEquals(1, colls.size());
assertEquals("optic1", colls.iterator().next());
});
}

@Test
public void uriColumnSpecified() {
List<RowRecord> rows = resultRows(op
.fromDocUris("/optic/test/musician1.json")
.lockForUpdate(op.col("uri")));
assertEquals(1, rows.size());
}

@Test
public void fromParamWithCustomUriColumn() {
ArrayNode paramValue = mapper.createArrayNode();
paramValue.addObject().put("myUri", "/optic/test/musician1.json");

List<RowRecord> rows = resultRows(op
.fromParam("bindingParam", "", op.colTypes(op.colType("myUri", "string")))
.lockForUpdate(op.col("myUri"))
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
assertEquals(1, rows.size());
}

@Test
public void fromParamWithQualifiedUriColumn() {
ArrayNode paramValue = mapper.createArrayNode();
paramValue.addObject().put("myUri", "/optic/test/musician1.json");

List<RowRecord> rows = resultRows(op
.fromParam("bindingParam", "myQualifier", op.colTypes(op.colType("myUri", "string")))
.lockForUpdate(op.viewCol("myQualifier", "myUri"))
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
assertEquals(1, rows.size());
}
// Verify that the collections were set based on the plan, which should have run second
verifyMetadata(uri, metadata -> {
DocumentMetadataHandle.DocumentCollections colls = metadata.getCollections();
assertEquals(1, colls.size());
assertEquals("optic1", colls.iterator().next());
});
}

@Test
void wrongEndpoint() {
rowManager.withUpdate(false);
assertThrows(
FailedRequestException.class,
() -> rowManager.execute(op.fromDocUris("/optic/test/musician1.json").lockForUpdate()),
"Hoping to update this assertion to verify that the server message tells the user to hit v1/rows/update " +
"instead; right now, it's mentioning using declareUpdate() which isn't applicable to a REST API user."
);
}

@Test
public void uriColumnSpecified() {
List<RowRecord> rows = resultRows(op
.fromDocUris("/optic/test/musician1.json")
.lockForUpdate(op.col("uri")));
assertEquals(1, rows.size());
}

@Test
public void fromParamWithCustomUriColumn() {
ArrayNode paramValue = mapper.createArrayNode();
paramValue.addObject().put("myUri", "/optic/test/musician1.json");

List<RowRecord> rows = resultRows(op
.fromParam("bindingParam", "", op.colTypes(op.colType("myUri", "string")))
.lockForUpdate(op.col("myUri"))
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
assertEquals(1, rows.size());
}

@Test
public void fromParamWithQualifiedUriColumn() {
ArrayNode paramValue = mapper.createArrayNode();
paramValue.addObject().put("myUri", "/optic/test/musician1.json");

List<RowRecord> rows = resultRows(op
.fromParam("bindingParam", "myQualifier", op.colTypes(op.colType("myUri", "string")))
.lockForUpdate(op.viewCol("myQualifier", "myUri"))
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
assertEquals(1, rows.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ void deleteNewMusician() {

@Test
void testResultRowsWithPointInTimeQueryTimestamp() {
rowManager.withUpdate(false);

final RawQueryDSLPlan plan = rowManager.newRawQueryDSLPlan(new StringHandle("op.fromView('opticUnitTest', 'musician_ml10')"));

JacksonHandle result = new JacksonHandle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,14 +552,14 @@ public void testErrorWhileStreamingRows() {
"occurred due to a bad request by the user, since the query was valid in the sense that it could be " +
"executed");

assertEquals("SQL-TABLENOTFOUND", ex.getServerMessage(),
"The server error message is expected to be the value of the 'ml-error-message' trailer");

assertEquals(
"Local message: failed to apply resource at rows: SQL-TABLENOTFOUND, Internal Server Error. Server Message: SQL-TABLENOTFOUND",
ex.getMessage(),
"The exception message is expected to be a formatted message containing the values of the 'ml-error-code' and " +
"'ml-error-message' trailers");
// For 11-nightly, changed these to be less precise as the server error message is free to change between minor
// releases, thus making any equality assertions very fragile.
assertTrue(ex.getServerMessage().contains("SQL-TABLENOTFOUND"),
"The server error message is expected to be the value of the 'ml-error-message' trailer");
assertTrue(
ex.getMessage().contains("SQL-TABLENOTFOUND"),
"The exception message is expected to be a formatted message containing the values of the 'ml-error-code' and " +
"'ml-error-message' trailers");
}

@Test
Expand Down Expand Up @@ -1247,8 +1247,9 @@ public void testAggregates() throws IOException {

recordRowSet.close();
}

@Test
public void testMapper() throws IOException, XPathExpressionException {
public void testMapper() {
RowManager rowMgr = Common.client.newRowManager();

PlanBuilder p = rowMgr.newPlanBuilder();
Expand All @@ -1260,13 +1261,11 @@ public void testMapper() throws IOException, XPathExpressionException {
.limit(3)
.map(p.resolveFunction(p.xs.QName("secondsMapper"), "/etc/optic/test/processors.sjs"));

int rowNum = 0;
for (RowRecord row: rowMgr.resultRows(builtPlan)) {
assertNotNull(row.getInt("rowNum"));
assertNotNull(row.getString("city"));
int seconds = row.getInt("seconds");
assertTrue(0 <= seconds && seconds < 60);
rowNum++;
}

builtPlan =
Expand All @@ -1279,13 +1278,11 @@ public void testMapper() throws IOException, XPathExpressionException {
p.xs.string("/etc/optic/test/processors.xqy")
));

rowNum = 0;
for (RowRecord row: rowMgr.resultRows(builtPlan)) {
assertNotNull(row.getInt("rowNum"));
assertNotNull(row.getString("city"));
int seconds = row.getInt("seconds");
assertTrue(0 <= seconds && seconds < 60);
rowNum++;
}
}

Expand Down Expand Up @@ -1319,7 +1316,7 @@ public void testColumnInfo() {
}

@Test
public void testGenerateView() throws IOException {
public void testGenerateView() {
RowManager rowMgr = Common.client.newRowManager();

PlanBuilder p = rowMgr.newPlanBuilder();
Expand Down