Skip to content

Commit e4a7eaf

Browse files
authored
Merge pull request #1630 from marklogic/feature/4119-optic-update
MLE-4119: User can now submit an Optic update plan
2 parents 15bd554 + 2528119 commit e4a7eaf

File tree

7 files changed

+134
-99
lines changed

7 files changed

+134
-99
lines changed

marklogic-client-api/src/main/java/com/marklogic/client/impl/RowManagerImpl.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public class RowManagerImpl
6262
private RowStructure rowStructureStyle = null;
6363
private Integer optimize;
6464
private String traceLabel;
65+
private boolean update;
6566

6667
public RowManagerImpl(RESTServices services) {
6768
super();
@@ -124,7 +125,13 @@ public void setOptimize(Integer value) {
124125
this.optimize = value;
125126
}
126127

127-
@Override
128+
@Override
129+
public RowManager withUpdate(boolean update) {
130+
this.update = update;
131+
return this;
132+
}
133+
134+
@Override
128135
public RawPlanDefinition newRawPlanDefinition(JSONWriteHandle handle) {
129136
return new RawPlanDefinitionImpl(handle);
130137
}
@@ -176,7 +183,11 @@ public <T extends StructureReadHandle> T resultDoc(Plan plan, T resultsHandle, T
176183
.withColumnTypes(getDatatypeStyle())
177184
.withOutput(getRowStructureStyle())
178185
.getRequestParameters();
179-
return services.postResource(requestLogger, "rows", transaction, params, astHandle, resultsHandle);
186+
return services.postResource(requestLogger, determinePath(), transaction, params, astHandle, resultsHandle);
187+
}
188+
189+
private String determinePath() {
190+
return this.update ? "rows/update" : "rows";
180191
}
181192

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

294-
return services.postResource(requestLogger, "rows", null, params, astHandle, resultsHandle);
305+
return services.postResource(requestLogger, determinePath(), null, params, astHandle, resultsHandle);
295306
}
307+
296308
@Override
297309
public <T> T explainAs(Plan plan, Class<T> as) {
298310
ContentHandle<T> handle = handleFor(as);
@@ -409,11 +421,12 @@ private <T extends AbstractReadHandle> String getRowFormat(T rowHandle) {
409421
private RESTServiceResultIterator submitPlan(PlanBuilderBaseImpl.RequestPlan requestPlan, RequestParameters params, Transaction transaction) {
410422
AbstractWriteHandle astHandle = requestPlan.getHandle();
411423
List<ContentParam> contentParams = requestPlan.getContentParams();
424+
final String path = determinePath();
412425
if (contentParams != null && !contentParams.isEmpty()) {
413426
contentParams.add(new ContentParam(new PlanBuilderBaseImpl.PlanParamBase("query"), astHandle, null));
414-
return services.postMultipartForm(requestLogger, "rows", transaction, params, contentParams);
427+
return services.postMultipartForm(requestLogger, path, transaction, params, contentParams);
415428
}
416-
return services.postIteratedResource(requestLogger, "rows", transaction, params, astHandle);
429+
return services.postIteratedResource(requestLogger, path, transaction, params, astHandle);
417430
}
418431

419432
private PlanBuilderBaseImpl.RequestPlan checkPlan(Plan plan) {

marklogic-client-api/src/main/java/com/marklogic/client/row/RowManager.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ public enum RowStructure{ARRAY, OBJECT}
8787
*/
8888
void setTraceLabel(String label);
8989

90+
/**
91+
* As of MarkLogic 11.2, the "v1/rows/update" endpoint must be used in order to submit an Optic plan that performs
92+
* an update. This method must be called with a value of {@code true} in order for that endpoint to be used instead
93+
* of "v1/rows". You may later call this method with a value of {@code false} in order to submit a plan that does
94+
* not perform an update.
95+
*
96+
* @param update set to {@code true} if submitting a plan that performs an update
97+
* @return the instance of this class
98+
* @since 6.5.0
99+
*/
100+
RowManager withUpdate(boolean update);
101+
90102
/**
91103
* @return the label that will be used for all log messages associated with the "optic" trace event
92104
*/

marklogic-client-api/src/test/java/com/marklogic/client/test/rows/AbstractOpticUpdateTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void setup() {
4747
.evalAs(String.class);
4848

4949
Common.client = Common.newClientBuilder().withUsername("writer-no-default-permissions").build();
50-
rowManager = Common.client.newRowManager();
50+
rowManager = Common.client.newRowManager().withUpdate(true);
5151
op = rowManager.newPlanBuilder();
5252
}
5353

marklogic-client-api/src/test/java/com/marklogic/client/test/rows/FromDocDescriptorsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void insertDocsWithUserWithDefaultCollectionsAndPermissions() {
4646
mapper.createObjectNode().put("hello", "two")));
4747

4848
Common.client = Common.newClientBuilder().withUsername(USER_WITH_DEFAULT_COLLECTIONS_AND_PERMISSIONS).build();
49-
Common.client.newRowManager().execute(op
49+
Common.client.newRowManager().withUpdate(true).execute(op
5050
.fromDocDescriptors(op.docDescriptors(writeSet))
5151
.write());
5252

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

9494
verifyOnlyDocCanBeUpdatedWithoutLosingAnyMetadata();
Lines changed: 88 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.marklogic.client.test.rows;
22

33
import com.fasterxml.jackson.databind.node.ArrayNode;
4+
import com.marklogic.client.FailedRequestException;
45
import com.marklogic.client.expression.PlanBuilder;
56
import com.marklogic.client.io.DocumentMetadataHandle;
67
import com.marklogic.client.io.JacksonHandle;
@@ -12,89 +13,99 @@
1213

1314
import java.util.List;
1415

15-
import static org.junit.jupiter.api.Assertions.assertEquals;
16-
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
import static org.junit.jupiter.api.Assertions.*;
1717

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

21-
@Test
22-
public void basicTest() {
23-
final String uri = "/acme/doc1.json";
24-
25-
// Write a document
26-
rowManager.execute(op.fromDocDescriptors(
27-
op.docDescriptor(newWriteOp(uri, new JacksonHandle(mapper.createObjectNode().put("hello", "world")))))
28-
.write());
29-
verifyJsonDoc(uri, doc -> assertEquals("world", doc.get("hello").asText()));
30-
31-
// Construct a plan that will lock the URI and update its collection
32-
PlanBuilder.ModifyPlan plan = op
33-
.fromDocDescriptors(
34-
op.docDescriptor(newWriteOp(uri, new DocumentMetadataHandle().withCollections("optic1"), null))
35-
)
36-
.lockForUpdate()
37-
.write(op.docCols(null, op.xs.stringSeq("uri", "collections")));
38-
39-
// Run an eval that locks the URI and sleeps for 2 seconds, which will block the plan run below
40-
new Thread(() -> {
41-
Common.newServerAdminClient().newServerEval()
42-
.javascript(String.format("declareUpdate(); " +
43-
"xdmp.lockForUpdate('%s'); " +
44-
"xdmp.sleep(2000); " +
45-
"xdmp.documentSetCollections('%s', ['eval1']);", uri, uri))
46-
.evalAs(String.class);
47-
}).start();
48-
49-
// Immediately run a plan that updates the collections as well; this should be blocked while the eval thread
50-
// above completes
51-
long start = System.currentTimeMillis();
52-
rowManager.execute(plan);
53-
long duration = System.currentTimeMillis() - start;
54-
System.out.println("DUR: " + duration);
55-
56-
assertTrue(duration > 1500,
21+
@Test
22+
public void basicTest() {
23+
final String uri = "/acme/doc1.json";
24+
25+
// Write a document
26+
rowManager.execute(op.fromDocDescriptors(
27+
op.docDescriptor(newWriteOp(uri, new JacksonHandle(mapper.createObjectNode().put("hello", "world")))))
28+
.write());
29+
verifyJsonDoc(uri, doc -> assertEquals("world", doc.get("hello").asText()));
30+
31+
// Construct a plan that will lock the URI and update its collection
32+
PlanBuilder.ModifyPlan plan = op
33+
.fromDocDescriptors(
34+
op.docDescriptor(newWriteOp(uri, new DocumentMetadataHandle().withCollections("optic1"), null))
35+
)
36+
.lockForUpdate()
37+
.write(op.docCols(null, op.xs.stringSeq("uri", "collections")));
38+
39+
// Run an eval that locks the URI and sleeps for 2 seconds, which will block the plan run below
40+
new Thread(() -> {
41+
Common.newServerAdminClient().newServerEval()
42+
.javascript(String.format("declareUpdate(); " +
43+
"xdmp.lockForUpdate('%s'); " +
44+
"xdmp.sleep(2000); " +
45+
"xdmp.documentSetCollections('%s', ['eval1']);", uri, uri))
46+
.evalAs(String.class);
47+
}).start();
48+
49+
// Immediately run a plan that updates the collections as well; this should be blocked while the eval thread
50+
// above completes
51+
long start = System.currentTimeMillis();
52+
rowManager.execute(plan);
53+
long duration = System.currentTimeMillis() - start;
54+
System.out.println("DUR: " + duration);
55+
56+
assertTrue(duration > 1500,
5757
"Because the eval call slept for 2 seconds, the duration of the plan execution should be at least " +
5858
"1500ms, which is much longer than normal; it may not be at least 2 seconds due to the small delay in " +
5959
"the Java layer of executing the plan; duration: " + duration);
6060

61-
// Verify that the collections were set based on the plan, which should have run second
62-
verifyMetadata(uri, metadata -> {
63-
DocumentMetadataHandle.DocumentCollections colls = metadata.getCollections();
64-
assertEquals(1, colls.size());
65-
assertEquals("optic1", colls.iterator().next());
66-
});
67-
}
68-
69-
@Test
70-
public void uriColumnSpecified() {
71-
List<RowRecord> rows = resultRows(op
72-
.fromDocUris("/optic/test/musician1.json")
73-
.lockForUpdate(op.col("uri")));
74-
assertEquals(1, rows.size());
75-
}
76-
77-
@Test
78-
public void fromParamWithCustomUriColumn() {
79-
ArrayNode paramValue = mapper.createArrayNode();
80-
paramValue.addObject().put("myUri", "/optic/test/musician1.json");
81-
82-
List<RowRecord> rows = resultRows(op
83-
.fromParam("bindingParam", "", op.colTypes(op.colType("myUri", "string")))
84-
.lockForUpdate(op.col("myUri"))
85-
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
86-
assertEquals(1, rows.size());
87-
}
88-
89-
@Test
90-
public void fromParamWithQualifiedUriColumn() {
91-
ArrayNode paramValue = mapper.createArrayNode();
92-
paramValue.addObject().put("myUri", "/optic/test/musician1.json");
93-
94-
List<RowRecord> rows = resultRows(op
95-
.fromParam("bindingParam", "myQualifier", op.colTypes(op.colType("myUri", "string")))
96-
.lockForUpdate(op.viewCol("myQualifier", "myUri"))
97-
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
98-
assertEquals(1, rows.size());
99-
}
61+
// Verify that the collections were set based on the plan, which should have run second
62+
verifyMetadata(uri, metadata -> {
63+
DocumentMetadataHandle.DocumentCollections colls = metadata.getCollections();
64+
assertEquals(1, colls.size());
65+
assertEquals("optic1", colls.iterator().next());
66+
});
67+
}
68+
69+
@Test
70+
void wrongEndpoint() {
71+
rowManager.withUpdate(false);
72+
assertThrows(
73+
FailedRequestException.class,
74+
() -> rowManager.execute(op.fromDocUris("/optic/test/musician1.json").lockForUpdate()),
75+
"Hoping to update this assertion to verify that the server message tells the user to hit v1/rows/update " +
76+
"instead; right now, it's mentioning using declareUpdate() which isn't applicable to a REST API user."
77+
);
78+
}
79+
80+
@Test
81+
public void uriColumnSpecified() {
82+
List<RowRecord> rows = resultRows(op
83+
.fromDocUris("/optic/test/musician1.json")
84+
.lockForUpdate(op.col("uri")));
85+
assertEquals(1, rows.size());
86+
}
87+
88+
@Test
89+
public void fromParamWithCustomUriColumn() {
90+
ArrayNode paramValue = mapper.createArrayNode();
91+
paramValue.addObject().put("myUri", "/optic/test/musician1.json");
92+
93+
List<RowRecord> rows = resultRows(op
94+
.fromParam("bindingParam", "", op.colTypes(op.colType("myUri", "string")))
95+
.lockForUpdate(op.col("myUri"))
96+
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
97+
assertEquals(1, rows.size());
98+
}
99+
100+
@Test
101+
public void fromParamWithQualifiedUriColumn() {
102+
ArrayNode paramValue = mapper.createArrayNode();
103+
paramValue.addObject().put("myUri", "/optic/test/musician1.json");
104+
105+
List<RowRecord> rows = resultRows(op
106+
.fromParam("bindingParam", "myQualifier", op.colTypes(op.colType("myUri", "string")))
107+
.lockForUpdate(op.viewCol("myQualifier", "myUri"))
108+
.bindParam("bindingParam", new JacksonHandle(paramValue), null));
109+
assertEquals(1, rows.size());
110+
}
100111
}

marklogic-client-api/src/test/java/com/marklogic/client/test/rows/ResultRowsWithTimestampTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ void deleteNewMusician() {
3232

3333
@Test
3434
void testResultRowsWithPointInTimeQueryTimestamp() {
35+
rowManager.withUpdate(false);
36+
3537
final RawQueryDSLPlan plan = rowManager.newRawQueryDSLPlan(new StringHandle("op.fromView('opticUnitTest', 'musician_ml10')"));
3638

3739
JacksonHandle result = new JacksonHandle();

marklogic-client-api/src/test/java/com/marklogic/client/test/rows/RowManagerTest.java

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -552,14 +552,14 @@ public void testErrorWhileStreamingRows() {
552552
"occurred due to a bad request by the user, since the query was valid in the sense that it could be " +
553553
"executed");
554554

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

565565
@Test
@@ -1247,8 +1247,9 @@ public void testAggregates() throws IOException {
12471247

12481248
recordRowSet.close();
12491249
}
1250+
12501251
@Test
1251-
public void testMapper() throws IOException, XPathExpressionException {
1252+
public void testMapper() {
12521253
RowManager rowMgr = Common.client.newRowManager();
12531254

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

1263-
int rowNum = 0;
12641264
for (RowRecord row: rowMgr.resultRows(builtPlan)) {
12651265
assertNotNull(row.getInt("rowNum"));
12661266
assertNotNull(row.getString("city"));
12671267
int seconds = row.getInt("seconds");
12681268
assertTrue(0 <= seconds && seconds < 60);
1269-
rowNum++;
12701269
}
12711270

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

1282-
rowNum = 0;
12831281
for (RowRecord row: rowMgr.resultRows(builtPlan)) {
12841282
assertNotNull(row.getInt("rowNum"));
12851283
assertNotNull(row.getString("city"));
12861284
int seconds = row.getInt("seconds");
12871285
assertTrue(0 <= seconds && seconds < 60);
1288-
rowNum++;
12891286
}
12901287
}
12911288

@@ -1319,7 +1316,7 @@ public void testColumnInfo() {
13191316
}
13201317

13211318
@Test
1322-
public void testGenerateView() throws IOException {
1319+
public void testGenerateView() {
13231320
RowManager rowMgr = Common.client.newRowManager();
13241321

13251322
PlanBuilder p = rowMgr.newPlanBuilder();

0 commit comments

Comments
 (0)