diff --git a/buildSrc/src/main/resources/checkstyle_suppressions.xml b/buildSrc/src/main/resources/checkstyle_suppressions.xml index 82c0ce3b77c..4b43b766d38 100644 --- a/buildSrc/src/main/resources/checkstyle_suppressions.xml +++ b/buildSrc/src/main/resources/checkstyle_suppressions.xml @@ -177,7 +177,6 @@ - diff --git a/core/src/main/java/org/elasticsearch/action/update/UpdateHelper.java b/core/src/main/java/org/elasticsearch/action/update/UpdateHelper.java index 09fc0bccbf3..6ea50df6b5e 100644 --- a/core/src/main/java/org/elasticsearch/action/update/UpdateHelper.java +++ b/core/src/main/java/org/elasticsearch/action/update/UpdateHelper.java @@ -19,6 +19,7 @@ package org.elasticsearch.action.update; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.delete.DeleteRequest; @@ -79,11 +80,56 @@ public class UpdateHelper extends AbstractComponent { } /** - * Prepares an update request by converting it into an index or delete request or an update response (no action). + * Prepares an update request by converting it into an index or delete request or an update response (no action, in the event of a + * noop). */ @SuppressWarnings("unchecked") protected Result prepare(ShardId shardId, UpdateRequest request, final GetResult getResult, LongSupplier nowInMillis) { - if (!getResult.isExists()) { + if (getResult.isExists() == false) { + // If the document didn't exist, execute the update request as an upsert + return prepareUpsert(shardId, request, getResult, nowInMillis); + } else if (getResult.internalSourceRef() == null) { + // no source, we can't do anything, throw a failure... + throw new DocumentSourceMissingException(shardId, request.type(), request.id()); + } else if (request.script() == null && request.doc() != null) { + // The request has no script, it is a new doc that should be merged with the old document + return prepareUpdateIndexRequest(shardId, request, getResult, request.detectNoop()); + } else { + // The request has a script (or empty script), execute the script and prepare a new index request + return prepareUpdateScriptRequest(shardId, request, getResult, nowInMillis); + } + } + + /** + * Execute a scripted upsert, where there is an existing upsert document and a script to be executed. The script is executed and a new + * Tuple of operation and updated {@code _source} is returned. + */ + Tuple> executeScriptedUpsert(IndexRequest upsert, Script script, LongSupplier nowInMillis) { + Map upsertDoc = upsert.sourceAsMap(); + Map ctx = new HashMap<>(3); + // Tell the script that this is a create and not an update + ctx.put(ContextFields.OP, UpdateOpType.CREATE.toString()); + ctx.put(ContextFields.SOURCE, upsertDoc); + ctx.put(ContextFields.NOW, nowInMillis.getAsLong()); + ctx = executeScript(script, ctx); + + UpdateOpType operation = UpdateOpType.lenientFromString((String) ctx.get(ContextFields.OP), logger, script.getIdOrCode()); + Map newSource = (Map) ctx.get(ContextFields.SOURCE); + + if (operation != UpdateOpType.CREATE && operation != UpdateOpType.NONE) { + // Only valid options for an upsert script are "create" (the default) or "none", meaning abort upsert + logger.warn("Invalid upsert operation [{}] for script [{}], doing nothing...", operation, script.getIdOrCode()); + operation = UpdateOpType.NONE; + } + + return new Tuple<>(operation, newSource); + } + + /** + * Prepare the request for upsert, executing the upsert script if present, and returning a {@code Result} containing a new + * {@code IndexRequest} to be executed on the primary and replicas. + */ + Result prepareUpsert(ShardId shardId, UpdateRequest request, final GetResult getResult, LongSupplier nowInMillis) { if (request.upsertRequest() == null && !request.docAsUpsert()) { throw new DocumentMissingException(shardId, request.type(), request.id()); } @@ -91,123 +137,164 @@ public class UpdateHelper extends AbstractComponent { if (request.scriptedUpsert() && request.script() != null) { // Run the script to perform the create logic IndexRequest upsert = request.upsertRequest(); - Map upsertDoc = upsert.sourceAsMap(); - Map ctx = new HashMap<>(2); - // Tell the script that this is a create and not an update - ctx.put("op", "create"); - ctx.put("_source", upsertDoc); - ctx.put("_now", nowInMillis.getAsLong()); - ctx = executeScript(request.script, ctx); - - //Allow the script to abort the create by setting "op" to "none" - String scriptOpChoice = (String) ctx.get("op"); - - // Only valid options for an upsert script are "create" - // (the default) or "none", meaning abort upsert - if (!"create".equals(scriptOpChoice)) { - if (!"none".equals(scriptOpChoice)) { - logger.warn("Used upsert operation [{}] for script [{}], doing nothing...", scriptOpChoice, - request.script.getIdOrCode()); - } - UpdateResponse update = new UpdateResponse(shardId, getResult.getType(), getResult.getId(), - getResult.getVersion(), DocWriteResponse.Result.NOOP); - update.setGetResult(getResult); - return new Result(update, DocWriteResponse.Result.NOOP, upsertDoc, XContentType.JSON); + Tuple> upsertResult = executeScriptedUpsert(upsert, request.script, nowInMillis); + switch (upsertResult.v1()) { + case CREATE: + // Update the index request with the new "_source" + indexRequest.source(upsertResult.v2()); + break; + case NONE: + UpdateResponse update = new UpdateResponse(shardId, getResult.getType(), getResult.getId(), + getResult.getVersion(), DocWriteResponse.Result.NOOP); + update.setGetResult(getResult); + return new Result(update, DocWriteResponse.Result.NOOP, upsertResult.v2(), XContentType.JSON); + default: + // It's fine to throw an exception here, the leniency is handled/logged by `executeScriptedUpsert` + throw new IllegalArgumentException("unknown upsert operation, got: " + upsertResult.v1()); } - indexRequest.source((Map) ctx.get("_source")); } - indexRequest.index(request.index()).type(request.type()).id(request.id()) + indexRequest.index(request.index()) + .type(request.type()).id(request.id()).setRefreshPolicy(request.getRefreshPolicy()).routing(request.routing()) + .parent(request.parent()).timeout(request.timeout()).waitForActiveShards(request.waitForActiveShards()) // it has to be a "create!" - .create(true) - .setRefreshPolicy(request.getRefreshPolicy()) - .routing(request.routing()) - .parent(request.parent()) - .timeout(request.timeout()) - .waitForActiveShards(request.waitForActiveShards()); + .create(true); + if (request.versionType() != VersionType.INTERNAL) { // in all but the internal versioning mode, we want to create the new document using the given version. indexRequest.version(request.version()).versionType(request.versionType()); } + return new Result(indexRequest, DocWriteResponse.Result.CREATED, null, null); - } - - long updateVersion = getResult.getVersion(); + } + /** + * Calculate the version to use for the update request, using either the existing version if internal versioning is used, or the get + * result document's version if the version type is "FORCE". + */ + static long calculateUpdateVersion(UpdateRequest request, GetResult getResult) { if (request.versionType() != VersionType.INTERNAL) { assert request.versionType() == VersionType.FORCE; - updateVersion = request.version(); // remember, match_any is excluded by the conflict test + return request.version(); // remember, match_any is excluded by the conflict test + } else { + return getResult.getVersion(); } + } - if (getResult.internalSourceRef() == null) { - // no source, we can't do nothing, through a failure... - throw new DocumentSourceMissingException(shardId, request.type(), request.id()); + /** + * Calculate a routing value to be used, either the included index request's routing, or retrieved document's routing when defined. + */ + @Nullable + static String calculateRouting(GetResult getResult, @Nullable IndexRequest updateIndexRequest) { + if (updateIndexRequest != null && updateIndexRequest.routing() != null) { + return updateIndexRequest.routing(); + } else if (getResult.getFields().containsKey(RoutingFieldMapper.NAME)) { + return getResult.field(RoutingFieldMapper.NAME).getValue().toString(); + } else { + return null; } + } - Tuple> sourceAndContent = XContentHelper.convertToMap(getResult.internalSourceRef(), true); - String operation = null; - final Map updatedSourceAsMap; + /** + * Calculate a parent value to be used, either the included index request's parent, or retrieved document's parent when defined. + */ + @Nullable + static String calculateParent(GetResult getResult, @Nullable IndexRequest updateIndexRequest) { + if (updateIndexRequest != null && updateIndexRequest.parent() != null) { + return updateIndexRequest.parent(); + } else if (getResult.getFields().containsKey(ParentFieldMapper.NAME)) { + return getResult.field(ParentFieldMapper.NAME).getValue().toString(); + } else { + return null; + } + } + + /** + * Prepare the request for merging the existing document with a new one, can optionally detect a noop change. Returns a {@code Result} + * containing a new {@code IndexRequest} to be executed on the primary and replicas. + */ + Result prepareUpdateIndexRequest(ShardId shardId, UpdateRequest request, GetResult getResult, boolean detectNoop) { + final long updateVersion = calculateUpdateVersion(request, getResult); + final IndexRequest currentRequest = request.doc(); + final String routing = calculateRouting(getResult, currentRequest); + final String parent = calculateParent(getResult, currentRequest); + final Tuple> sourceAndContent = XContentHelper.convertToMap(getResult.internalSourceRef(), true); final XContentType updateSourceContentType = sourceAndContent.v1(); - String routing = getResult.getFields().containsKey(RoutingFieldMapper.NAME) ? getResult.field(RoutingFieldMapper.NAME).getValue().toString() : null; - String parent = getResult.getFields().containsKey(ParentFieldMapper.NAME) ? getResult.field(ParentFieldMapper.NAME).getValue().toString() : null; + final Map updatedSourceAsMap = sourceAndContent.v2(); - if (request.script() == null && request.doc() != null) { - IndexRequest indexRequest = request.doc(); - updatedSourceAsMap = sourceAndContent.v2(); - if (indexRequest.routing() != null) { - routing = indexRequest.routing(); - } - if (indexRequest.parent() != null) { - parent = indexRequest.parent(); - } - boolean noop = !XContentHelper.update(updatedSourceAsMap, indexRequest.sourceAsMap(), request.detectNoop()); - // noop could still be true even if detectNoop isn't because update detects empty maps as noops. BUT we can only - // actually turn the update into a noop if detectNoop is true to preserve backwards compatibility and to handle - // cases where users repopulating multi-fields or adding synonyms, etc. - if (request.detectNoop() && noop) { - operation = "none"; - } + final boolean noop = !XContentHelper.update(updatedSourceAsMap, currentRequest.sourceAsMap(), detectNoop); + + // We can only actually turn the update into a noop if detectNoop is true to preserve backwards compatibility and to handle cases + // where users repopulating multi-fields or adding synonyms, etc. + if (detectNoop && noop) { + UpdateResponse update = new UpdateResponse(shardId, getResult.getType(), getResult.getId(), + getResult.getVersion(), DocWriteResponse.Result.NOOP); + update.setGetResult(extractGetResult(request, request.index(), getResult.getVersion(), updatedSourceAsMap, + updateSourceContentType, getResult.internalSourceRef())); + return new Result(update, DocWriteResponse.Result.NOOP, updatedSourceAsMap, updateSourceContentType); } else { - Map ctx = new HashMap<>(16); - ctx.put("_index", getResult.getIndex()); - ctx.put("_type", getResult.getType()); - ctx.put("_id", getResult.getId()); - ctx.put("_version", getResult.getVersion()); - ctx.put("_routing", routing); - ctx.put("_parent", parent); - ctx.put("_source", sourceAndContent.v2()); - ctx.put("_now", nowInMillis.getAsLong()); - - ctx = executeScript(request.script, ctx); - - operation = (String) ctx.get("op"); - - updatedSourceAsMap = (Map) ctx.get("_source"); + final IndexRequest finalIndexRequest = Requests.indexRequest(request.index()) + .type(request.type()).id(request.id()).routing(routing).parent(parent) + .source(updatedSourceAsMap, updateSourceContentType).version(updateVersion).versionType(request.versionType()) + .waitForActiveShards(request.waitForActiveShards()).timeout(request.timeout()) + .setRefreshPolicy(request.getRefreshPolicy()); + return new Result(finalIndexRequest, DocWriteResponse.Result.UPDATED, updatedSourceAsMap, updateSourceContentType); } + } + + /** + * Prepare the request for updating an existing document using a script. Executes the script and returns a {@code Result} containing + * either a new {@code IndexRequest} or {@code DeleteRequest} (depending on the script's returned "op" value) to be executed on the + * primary and replicas. + */ + Result prepareUpdateScriptRequest(ShardId shardId, UpdateRequest request, GetResult getResult, LongSupplier nowInMillis) { + final long updateVersion = calculateUpdateVersion(request, getResult); + final IndexRequest currentRequest = request.doc(); + final String routing = calculateRouting(getResult, currentRequest); + final String parent = calculateParent(getResult, currentRequest); + final Tuple> sourceAndContent = XContentHelper.convertToMap(getResult.internalSourceRef(), true); + final XContentType updateSourceContentType = sourceAndContent.v1(); + final Map sourceAsMap = sourceAndContent.v2(); + + Map ctx = new HashMap<>(16); + ctx.put(ContextFields.OP, UpdateOpType.INDEX.toString()); // The default operation is "index" + ctx.put(ContextFields.INDEX, getResult.getIndex()); + ctx.put(ContextFields.TYPE, getResult.getType()); + ctx.put(ContextFields.ID, getResult.getId()); + ctx.put(ContextFields.VERSION, getResult.getVersion()); + ctx.put(ContextFields.ROUTING, routing); + ctx.put(ContextFields.PARENT, parent); + ctx.put(ContextFields.SOURCE, sourceAsMap); + ctx.put(ContextFields.NOW, nowInMillis.getAsLong()); + + ctx = executeScript(request.script, ctx); + + UpdateOpType operation = UpdateOpType.lenientFromString((String) ctx.get(ContextFields.OP), logger, request.script.getIdOrCode()); + + final Map updatedSourceAsMap = (Map) ctx.get(ContextFields.SOURCE); + + switch (operation) { + case INDEX: + final IndexRequest indexRequest = Requests.indexRequest(request.index()) + .type(request.type()).id(request.id()).routing(routing).parent(parent) + .source(updatedSourceAsMap, updateSourceContentType).version(updateVersion).versionType(request.versionType()) + .waitForActiveShards(request.waitForActiveShards()).timeout(request.timeout()) + .setRefreshPolicy(request.getRefreshPolicy()); + return new Result(indexRequest, DocWriteResponse.Result.UPDATED, updatedSourceAsMap, updateSourceContentType); + case DELETE: + DeleteRequest deleteRequest = Requests.deleteRequest(request.index()) + .type(request.type()).id(request.id()).routing(routing).parent(parent) + .version(updateVersion).versionType(request.versionType()).waitForActiveShards(request.waitForActiveShards()) + .timeout(request.timeout()).setRefreshPolicy(request.getRefreshPolicy()); + return new Result(deleteRequest, DocWriteResponse.Result.DELETED, updatedSourceAsMap, updateSourceContentType); + default: + // If it was neither an INDEX or DELETE operation, treat it as a noop + UpdateResponse update = new UpdateResponse(shardId, getResult.getType(), getResult.getId(), + getResult.getVersion(), DocWriteResponse.Result.NOOP); + update.setGetResult(extractGetResult(request, request.index(), getResult.getVersion(), updatedSourceAsMap, + updateSourceContentType, getResult.internalSourceRef())); + return new Result(update, DocWriteResponse.Result.NOOP, updatedSourceAsMap, updateSourceContentType); - if (operation == null || "index".equals(operation)) { - final IndexRequest indexRequest = Requests.indexRequest(request.index()).type(request.type()).id(request.id()).routing(routing).parent(parent) - .source(updatedSourceAsMap, updateSourceContentType) - .version(updateVersion).versionType(request.versionType()) - .waitForActiveShards(request.waitForActiveShards()) - .timeout(request.timeout()) - .setRefreshPolicy(request.getRefreshPolicy()); - return new Result(indexRequest, DocWriteResponse.Result.UPDATED, updatedSourceAsMap, updateSourceContentType); - } else if ("delete".equals(operation)) { - DeleteRequest deleteRequest = Requests.deleteRequest(request.index()).type(request.type()).id(request.id()).routing(routing).parent(parent) - .version(updateVersion).versionType(request.versionType()) - .waitForActiveShards(request.waitForActiveShards()) - .timeout(request.timeout()) - .setRefreshPolicy(request.getRefreshPolicy()); - return new Result(deleteRequest, DocWriteResponse.Result.DELETED, updatedSourceAsMap, updateSourceContentType); - } else if ("none".equals(operation)) { - UpdateResponse update = new UpdateResponse(shardId, getResult.getType(), getResult.getId(), getResult.getVersion(), DocWriteResponse.Result.NOOP); - update.setGetResult(extractGetResult(request, request.index(), getResult.getVersion(), updatedSourceAsMap, updateSourceContentType, getResult.internalSourceRef())); - return new Result(update, DocWriteResponse.Result.NOOP, updatedSourceAsMap, updateSourceContentType); - } else { - logger.warn("Used update operation [{}] for script [{}], doing nothing...", operation, request.script.getIdOrCode()); - UpdateResponse update = new UpdateResponse(shardId, getResult.getType(), getResult.getId(), getResult.getVersion(), DocWriteResponse.Result.NOOP); - return new Result(update, DocWriteResponse.Result.NOOP, updatedSourceAsMap, updateSourceContentType); } } @@ -216,7 +303,7 @@ public class UpdateHelper extends AbstractComponent { if (scriptService != null) { CompiledScript compiledScript = scriptService.compile(script, ScriptContext.Standard.UPDATE); ExecutableScript executableScript = scriptService.executable(compiledScript, script.getParams()); - executableScript.setNextVar("ctx", ctx); + executableScript.setNextVar(ContextFields.CTX, ctx); executableScript.run(); } } catch (Exception e) { @@ -229,7 +316,8 @@ public class UpdateHelper extends AbstractComponent { * Applies {@link UpdateRequest#fetchSource()} to the _source of the updated document to be returned in a update response. * For BWC this function also extracts the {@link UpdateRequest#fields()} from the updated document to be returned in a update response */ - public GetResult extractGetResult(final UpdateRequest request, String concreteIndex, long version, final Map source, XContentType sourceContentType, @Nullable final BytesReference sourceAsBytes) { + public GetResult extractGetResult(final UpdateRequest request, String concreteIndex, long version, final Map source, + XContentType sourceContentType, @Nullable final BytesReference sourceAsBytes) { if ((request.fields() == null || request.fields().length == 0) && (request.fetchSource() == null || request.fetchSource().fetchSource() == false)) { return null; @@ -278,7 +366,8 @@ public class UpdateHelper extends AbstractComponent { } // TODO when using delete/none, we can still return the source as bytes by generating it (using the sourceContentType) - return new GetResult(concreteIndex, request.type(), request.id(), version, true, sourceRequested ? sourceFilteredAsBytes : null, fields); + return new GetResult(concreteIndex, request.type(), request.id(), version, true, + sourceRequested ? sourceFilteredAsBytes : null, fields); } public static class Result { @@ -288,7 +377,8 @@ public class UpdateHelper extends AbstractComponent { private final Map updatedSourceAsMap; private final XContentType updateSourceContentType; - public Result(Streamable action, DocWriteResponse.Result result, Map updatedSourceAsMap, XContentType updateSourceContentType) { + public Result(Streamable action, DocWriteResponse.Result result, Map updatedSourceAsMap, + XContentType updateSourceContentType) { this.action = action; this.result = result; this.updatedSourceAsMap = updatedSourceAsMap; @@ -313,4 +403,58 @@ public class UpdateHelper extends AbstractComponent { } } + /** + * After executing the script, this is the type of operation that will be used for subsequent actions. This corresponds to the "ctx.op" + * variable inside of scripts. + */ + enum UpdateOpType { + CREATE("create"), + INDEX("index"), + DELETE("delete"), + NONE("none"); + + private final String name; + + UpdateOpType(String name) { + this.name = name; + } + + public static UpdateOpType lenientFromString(String operation, Logger logger, String scriptId) { + switch (operation) { + case "create": + return UpdateOpType.CREATE; + case "index": + return UpdateOpType.INDEX; + case "delete": + return UpdateOpType.DELETE; + case "none": + return UpdateOpType.NONE; + default: + // TODO: can we remove this leniency yet?? + logger.warn("Used upsert operation [{}] for script [{}], doing nothing...", operation, scriptId); + return UpdateOpType.NONE; + } + } + + @Override + public String toString() { + return name; + } + } + + /** + * Field names used to populate the script context + */ + public static class ContextFields { + public static final String CTX = "ctx"; + public static final String OP = "op"; + public static final String SOURCE = "_source"; + public static final String NOW = "_now"; + public static final String INDEX = "_index"; + public static final String TYPE = "_type"; + public static final String ID = "_id"; + public static final String VERSION = "_version"; + public static final String ROUTING = "_routing"; + public static final String PARENT = "_parent"; + } } diff --git a/core/src/test/java/org/elasticsearch/action/update/UpdateRequestTests.java b/core/src/test/java/org/elasticsearch/action/update/UpdateRequestTests.java index 339976a7086..6cdc5c6bdac 100644 --- a/core/src/test/java/org/elasticsearch/action/update/UpdateRequestTests.java +++ b/core/src/test/java/org/elasticsearch/action/update/UpdateRequestTests.java @@ -20,6 +20,8 @@ package org.elasticsearch.action.update; import org.elasticsearch.Version; +import org.elasticsearch.action.DocWriteResponse; +import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.replication.ReplicationRequest; import org.elasticsearch.common.bytes.BytesArray; @@ -34,6 +36,8 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.env.Environment; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.get.GetField; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.script.MockScriptEngine; @@ -50,6 +54,7 @@ import org.junit.Before; import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -92,6 +97,16 @@ public class UpdateRequestTests extends ESTestCase { source.put("update_timestamp", ctx.get("_now")); return null; }); + scripts.put( + "ctx._source.body = \"foo\"", + vars -> { + @SuppressWarnings("unchecked") + final Map ctx = (Map) vars.get("ctx"); + @SuppressWarnings("unchecked") + final Map source = (Map) ctx.get("_source"); + source.put("body", "foo"); + return null; + }); scripts.put( "ctx._timestamp = ctx._now", vars -> { @@ -108,6 +123,22 @@ public class UpdateRequestTests extends ESTestCase { ctx.put("op", "delete"); return null; }); + scripts.put( + "ctx.op = bad", + vars -> { + @SuppressWarnings("unchecked") + final Map ctx = (Map) vars.get("ctx"); + ctx.put("op", "bad"); + return null; + }); + scripts.put( + "ctx.op = none", + vars -> { + @SuppressWarnings("unchecked") + final Map ctx = (Map) vars.get("ctx"); + ctx.put("op", "none"); + return null; + }); scripts.put("return", vars -> null); final ScriptContextRegistry scriptContextRegistry = new ScriptContextRegistry(emptyList()); final MockScriptEngine engine = new MockScriptEngine("mock", scripts); @@ -502,4 +533,131 @@ public class UpdateRequestTests extends ESTestCase { updateRequest.upsert(new IndexRequest("index", "type", "1").version(1L)); assertThat(updateRequest.validate().validationErrors(), contains("can't provide version in upsert request")); } + + public void testParentAndRoutingExtraction() throws Exception { + GetResult getResult = new GetResult("test", "type", "1", 0, false, null, null); + IndexRequest indexRequest = new IndexRequest("test", "type", "1"); + + // There is no routing and parent because the document doesn't exist + assertNull(UpdateHelper.calculateRouting(getResult, null)); + assertNull(UpdateHelper.calculateParent(getResult, null)); + + // There is no routing and parent the indexing request + assertNull(UpdateHelper.calculateRouting(getResult, indexRequest)); + assertNull(UpdateHelper.calculateParent(getResult, indexRequest)); + + // Doc exists but has no source or fields + getResult = new GetResult("test", "type", "1", 0, true, null, null); + + // There is no routing and parent on either request + assertNull(UpdateHelper.calculateRouting(getResult, indexRequest)); + assertNull(UpdateHelper.calculateParent(getResult, indexRequest)); + + Map fields = new HashMap<>(); + fields.put("_parent", new GetField("_parent", Collections.singletonList("parent1"))); + fields.put("_routing", new GetField("_routing", Collections.singletonList("routing1"))); + + // Doc exists and has the parent and routing fields + getResult = new GetResult("test", "type", "1", 0, true, null, fields); + + // Use the get result parent and routing + assertThat(UpdateHelper.calculateRouting(getResult, indexRequest), equalTo("routing1")); + assertThat(UpdateHelper.calculateParent(getResult, indexRequest), equalTo("parent1")); + + // Index request has overriding parent and routing values + indexRequest = new IndexRequest("test", "type", "1").parent("parent2").routing("routing2"); + + // Use the request's parent and routing + assertThat(UpdateHelper.calculateRouting(getResult, indexRequest), equalTo("routing2")); + assertThat(UpdateHelper.calculateParent(getResult, indexRequest), equalTo("parent2")); + } + + @SuppressWarnings("deprecated") // VersionType.FORCE is deprecated + public void testCalculateUpdateVersion() throws Exception { + long randomVersion = randomIntBetween(0, 100); + GetResult getResult = new GetResult("test", "type", "1", randomVersion, true, new BytesArray("{}"), null); + + UpdateRequest request = new UpdateRequest("test", "type1", "1"); + long version = UpdateHelper.calculateUpdateVersion(request, getResult); + + // Use the get result's version + assertThat(version, equalTo(randomVersion)); + + request = new UpdateRequest("test", "type1", "1").versionType(VersionType.FORCE).version(1337); + version = UpdateHelper.calculateUpdateVersion(request, getResult); + + // Use the forced update request version + assertThat(version, equalTo(1337L)); + } + + public void testNoopDetection() throws Exception { + ShardId shardId = new ShardId("test", "", 0); + GetResult getResult = new GetResult("test", "type", "1", 0, true, + new BytesArray("{\"body\": \"foo\"}"), + null); + + UpdateRequest request = new UpdateRequest("test", "type1", "1").fromXContent( + createParser(JsonXContent.jsonXContent, new BytesArray("{\"doc\": {\"body\": \"foo\"}}"))); + + UpdateHelper.Result result = updateHelper.prepareUpdateIndexRequest(shardId, request, getResult, true); + + assertThat(result.action(), instanceOf(UpdateResponse.class)); + assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.NOOP)); + + // Try again, with detectNoop turned off + result = updateHelper.prepareUpdateIndexRequest(shardId, request, getResult, false); + assertThat(result.action(), instanceOf(IndexRequest.class)); + assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.UPDATED)); + assertThat(result.updatedSourceAsMap().get("body").toString(), equalTo("foo")); + + // Change the request to be a different doc + request = new UpdateRequest("test", "type1", "1").fromXContent( + createParser(JsonXContent.jsonXContent, new BytesArray("{\"doc\": {\"body\": \"bar\"}}"))); + result = updateHelper.prepareUpdateIndexRequest(shardId, request, getResult, true); + + assertThat(result.action(), instanceOf(IndexRequest.class)); + assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.UPDATED)); + assertThat(result.updatedSourceAsMap().get("body").toString(), equalTo("bar")); + + } + + public void testUpdateScript() throws Exception { + ShardId shardId = new ShardId("test", "", 0); + GetResult getResult = new GetResult("test", "type", "1", 0, true, + new BytesArray("{\"body\": \"bar\"}"), + null); + + UpdateRequest request = new UpdateRequest("test", "type1", "1") + .script(mockInlineScript("ctx._source.body = \"foo\"")); + + UpdateHelper.Result result = updateHelper.prepareUpdateScriptRequest(shardId, request, getResult, + ESTestCase::randomNonNegativeLong); + + assertThat(result.action(), instanceOf(IndexRequest.class)); + assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.UPDATED)); + assertThat(result.updatedSourceAsMap().get("body").toString(), equalTo("foo")); + + // Now where the script changes the op to "delete" + request = new UpdateRequest("test", "type1", "1").script(mockInlineScript("ctx.op = delete")); + + result = updateHelper.prepareUpdateScriptRequest(shardId, request, getResult, + ESTestCase::randomNonNegativeLong); + + assertThat(result.action(), instanceOf(DeleteRequest.class)); + assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.DELETED)); + + // We treat everything else as a No-op + boolean goodNoop = randomBoolean(); + if (goodNoop) { + request = new UpdateRequest("test", "type1", "1").script(mockInlineScript("ctx.op = none")); + } else { + request = new UpdateRequest("test", "type1", "1").script(mockInlineScript("ctx.op = bad")); + } + + result = updateHelper.prepareUpdateScriptRequest(shardId, request, getResult, + ESTestCase::randomNonNegativeLong); + + assertThat(result.action(), instanceOf(UpdateResponse.class)); + assertThat(result.getResponseResult(), equalTo(DocWriteResponse.Result.NOOP)); + } }