diff --git a/CHANGELOG.md b/CHANGELOG.md index 506df974b3..0e12a76ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Fix /_nodes/stats, /_nodes/info throwing serialization error ([#315](https://github.com/opensearch-project/opensearch-java/pull/315)) - Do not double-wrap OpenSearchException on error ([#323](https://github.com/opensearch-project/opensearch-java/pull/323)) - Fix AwsSdk2TransportOptions.responseCompression ([#322](https://github.com/opensearch-project/opensearch-java/pull/322)) +- Bulk UpdateOperation misses upsert options ([#353](https://github.com/opensearch-project/opensearch-java/pull/353)) ### Security diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperation.java b/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperation.java index 7679df457b..3e1c9dd890 100644 --- a/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperation.java +++ b/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperation.java @@ -15,7 +15,7 @@ * not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an @@ -39,6 +39,7 @@ import org.opensearch.client.json.JsonpMapper; import org.opensearch.client.json.JsonpSerializer; import org.opensearch.client.json.NdJsonpSerializable; +import org.opensearch.client.opensearch._types.Script; import org.opensearch.client.util.ApiTypeHelper; import org.opensearch.client.util.ObjectBuilder; import jakarta.json.stream.JsonGenerator; @@ -52,7 +53,7 @@ public class UpdateOperation extends BulkOperationBase implements NdJsonpSerializable, BulkOperationVariant { - private final TDocument document; + private final UpdateOperationData data; @Nullable private final Boolean requireAlias; @@ -67,8 +68,7 @@ public class UpdateOperation extends BulkOperationBase implements NdJ private UpdateOperation(Builder builder) { super(builder); - this.document = ApiTypeHelper.requireNonNull(builder.document, this, "document"); - + this.data = ApiTypeHelper.requireNonNull(builder.data, this, "data"); this.requireAlias = builder.requireAlias; this.retryOnConflict = builder.retryOnConflict; this.tDocumentSerializer = builder.tDocumentSerializer; @@ -88,16 +88,9 @@ public BulkOperation.Kind _bulkOperationKind() { return BulkOperation.Kind.Update; } - /** - * Required - API name: {@code document} - */ - public final TDocument document() { - return this.document; - } - @Override public Iterator _serializables() { - return Arrays.asList(this, this.document).iterator(); + return Arrays.asList(this, this.data).iterator(); } /** @@ -129,7 +122,6 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) { generator.write(this.retryOnConflict); } - } // --------------------------------------------------------------------------------------------- @@ -141,24 +133,61 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) { public static class Builder extends BulkOperationBase.AbstractBuilder> implements ObjectBuilder> { + + private UpdateOperationData data; + + @Nullable private TDocument document; + @Nullable + private Boolean requireAlias; + + @Nullable + private Integer retryOnConflict; + + @Nullable + private JsonpSerializer tDocumentSerializer; + + @Nullable + private Boolean docAsUpsert; + + @Nullable + private TDocument upsert; + + @Nullable + private Script script; + /** - * Required - API name: {@code document} + * API name: {@code document} */ public final Builder document(TDocument value) { this.document = value; return this; } - @Nullable - private Boolean requireAlias; + /** + * API name: {@code docAsUpsert} + */ + public final Builder docAsUpsert(@Nullable Boolean value) { + this.docAsUpsert = value; + return this; + } - @Nullable - private Integer retryOnConflict; + /** + * API name: {@code upsert} + */ + public final Builder upsert(@Nullable TDocument value) { + this.upsert = value; + return this; + } - @Nullable - private JsonpSerializer tDocumentSerializer; + /** + * API name: {@code script} + */ + public final Builder script(@Nullable Script value) { + this.script = value; + return this; + } /** * API name: {@code require_alias} @@ -194,11 +223,19 @@ protected Builder self() { * Builds a {@link UpdateOperation}. * * @throws NullPointerException - * if some of the required fields are null. + * if some of the required fields are null. */ public UpdateOperation build() { _checkSingleUse(); + data = new UpdateOperationData.Builder() + .document(document) + .docAsUpsert(docAsUpsert) + .script(script) + .upsert(upsert) + .tDocumentSerializer(tDocumentSerializer) + .build(); + return new UpdateOperation(this); } } diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperationData.java b/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperationData.java new file mode 100644 index 0000000000..e29ba6e630 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/core/bulk/UpdateOperationData.java @@ -0,0 +1,158 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.core.bulk; + +import javax.annotation.Nullable; + +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.JsonpSerializable; +import org.opensearch.client.json.JsonpSerializer; +import org.opensearch.client.json.JsonpUtils; +import org.opensearch.client.opensearch._types.Script; +import org.opensearch.client.util.ObjectBuilder; + +import jakarta.json.stream.JsonGenerator; + +public class UpdateOperationData implements JsonpSerializable { + @Nullable + private final TDocument document; + + @Nullable + private final Boolean docAsUpsert; + + @Nullable + private final TDocument upsert; + + @Nullable + private final Script script; + + @Nullable + private final JsonpSerializer tDocumentSerializer; + + private UpdateOperationData(Builder builder) { + this.document = builder.document; + this.docAsUpsert = builder.docAsUpsert; + this.script = builder.script; + this.upsert = builder.upsert; + this.tDocumentSerializer = builder.tDocumentSerializer; + + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + generator.writeStartObject(); + serializeInternal(generator, mapper); + generator.writeEnd(); + } + + protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) { + if (this.docAsUpsert != null) { + generator.writeKey("doc_as_upsert"); + generator.write(this.docAsUpsert); + } + + if (this.document != null) { + generator.writeKey("doc"); + JsonpUtils.serialize(document, generator, tDocumentSerializer, mapper); + } + + if (this.upsert != null) { + generator.writeKey("upsert"); + JsonpUtils.serialize(upsert, generator, tDocumentSerializer, mapper); + } + + if (this.script != null) { + generator.writeKey("script"); + this.script.serialize(generator, mapper); + } + } + + /** + * Builder for {@link UpdateOperationData}. + */ + public static class Builder extends BulkOperationBase.AbstractBuilder> + implements + ObjectBuilder> { + + @Nullable + private TDocument document; + + @Nullable + private JsonpSerializer tDocumentSerializer; + + @Nullable + private Boolean docAsUpsert; + + @Nullable + private TDocument upsert; + + + @Nullable + private Script script; + + /** + * API name: {@code document} + */ + public final Builder document(TDocument value) { + this.document = value; + return this; + } + + + /** + * API name: {@code docAsUpsert} + */ + public final Builder docAsUpsert(@Nullable Boolean value) { + this.docAsUpsert = value; + return this; + } + + /** + * API name: {@code upsert} + */ + public final Builder upsert(@Nullable TDocument value) { + this.upsert = value; + return this; + } + + /** + * API name: {@code script} + */ + public final Builder script(@Nullable Script value) { + this.script = value; + return this; + } + + /** + * Serializer for TDocument. If not set, an attempt will be made to find a + * serializer from the JSON context. + */ + public final Builder tDocumentSerializer(@Nullable JsonpSerializer value) { + this.tDocumentSerializer = value; + return this; + } + + @Override + protected Builder self() { + return this; + } + + /** + * Builds a {@link UpdateOperationData}. + * + * @throws NullPointerException + * if some of the required fields are null. + */ + public UpdateOperationData build() { + _checkSingleUse(); + + return new UpdateOperationData(this); + } + } +} diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/integTest/AbstractCrudIT.java b/java-client/src/test/java/org/opensearch/client/opensearch/integTest/AbstractCrudIT.java index 380a5bdd8a..dbf45cd4e6 100644 --- a/java-client/src/test/java/org/opensearch/client/opensearch/integTest/AbstractCrudIT.java +++ b/java-client/src/test/java/org/opensearch/client/opensearch/integTest/AbstractCrudIT.java @@ -11,9 +11,12 @@ import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; +import org.opensearch.client.json.JsonData; +import org.opensearch.client.opensearch._types.InlineScript; import org.opensearch.client.opensearch._types.OpenSearchException; import org.opensearch.client.opensearch._types.Refresh; import org.opensearch.client.opensearch._types.Result; +import org.opensearch.client.opensearch._types.Script; import org.opensearch.client.opensearch.core.BulkRequest; import org.opensearch.client.opensearch.core.BulkResponse; import org.opensearch.client.opensearch.core.DeleteResponse; @@ -336,7 +339,7 @@ public void testBulk() throws IOException { boolean erroneous = randomBoolean(); errors[i] = erroneous; BulkOperation.Kind opType = randomFrom(BulkOperation.Kind.Delete, BulkOperation.Kind.Index, - BulkOperation.Kind.Create/*, BulkOperation.Kind.Update*/); + BulkOperation.Kind.Create, BulkOperation.Kind.Update); if (opType.equals(BulkOperation.Kind.Delete)) { if (!erroneous) { assertEquals( @@ -396,6 +399,93 @@ public void testBulk() throws IOException { validateBulkResponses(nbItems, errors, bulkResponse, bulkRequest); } + public void testBulkUpdateScript() throws IOException { + final String id = "100"; + + final AppData appData = new AppData(); + appData.setIntValue(1337); + appData.setMsg("foo"); + + assertEquals( + Result.Created, + javaClient().index(b -> b.index("index").id(id).document(appData)).result() + ); + + final BulkOperation op = new BulkOperation.Builder().update(o -> o + .index("index") + .id(id) + .script(Script.of(s -> s.inline(new InlineScript.Builder() + .lang("painless") + .source("ctx._source.intValue += params.inc") + .params("inc", JsonData.of(1)) + .build()))) + ).build(); + + BulkRequest bulkRequest = new BulkRequest.Builder().operations(op).build(); + BulkResponse bulkResponse = javaClient().bulk(bulkRequest); + + assertTrue(bulkResponse.took() > 0); + assertEquals(1, bulkResponse.items().size()); + + final GetResponse getResponse = javaClient().get(b -> b.index("index").id(id), AppData.class); + assertTrue(getResponse.found()); + assertEquals(1338, getResponse.source().getIntValue()); + } + + public void testBulkUpdateScriptUpsert() throws IOException { + final String id = "100"; + + final AppData appData = new AppData(); + appData.setIntValue(1337); + appData.setMsg("foo"); + + final BulkOperation op = new BulkOperation.Builder().update(o -> o + .index("index") + .id(id) + .upsert(appData) + .script(Script.of(s -> s.inline(new InlineScript.Builder() + .lang("painless") + .source("ctx._source.intValue += params.inc") + .params("inc", JsonData.of(1)) + .build()))) + ).build(); + + BulkRequest bulkRequest = new BulkRequest.Builder().operations(op).build(); + BulkResponse bulkResponse = javaClient().bulk(bulkRequest); + + assertTrue(bulkResponse.took() > 0); + assertEquals(1, bulkResponse.items().size()); + + final GetResponse getResponse = javaClient().get(b -> b.index("index").id(id), AppData.class); + assertTrue(getResponse.found()); + assertEquals(1337, getResponse.source().getIntValue()); + } + + public void testBulkUpdateUpsert() throws IOException { + final String id = "100"; + + final AppData appData = new AppData(); + appData.setIntValue(1337); + appData.setMsg("foo"); + + final BulkOperation op = new BulkOperation.Builder().update(o -> o + .index("index") + .id(id) + .document(new AppData()) + .upsert(appData) + ).build(); + + BulkRequest bulkRequest = new BulkRequest.Builder().operations(op).build(); + BulkResponse bulkResponse = javaClient().bulk(bulkRequest); + + assertTrue(bulkResponse.took() > 0); + assertEquals(1, bulkResponse.items().size()); + + final GetResponse getResponse = javaClient().get(b -> b.index("index").id(id), AppData.class); + assertTrue(getResponse.found()); + assertEquals(1337, getResponse.source().getIntValue()); + } + private void validateBulkResponses(int nbItems, boolean[] errors, BulkResponse bulkResponse, BulkRequest bulkRequest) { for (int i = 0; i < nbItems; i++) { BulkResponseItem bulkResponseItem = bulkResponse.items().get(i); @@ -407,8 +497,8 @@ private void validateBulkResponses(int nbItems, boolean[] errors, BulkResponse b if (bulkOperation.isIndex() || bulkOperation.isCreate()) { assertEquals(errors[i] ? 409 : 201, bulkResponseItem.status()); } else if (bulkOperation.isUpdate()) { - assertEquals(errors[i] ? Result.NotFound.jsonValue() : Result.Updated.jsonValue(), bulkResponseItem.result()); assertEquals(errors[i] ? 404 : 200, bulkResponseItem.status()); + assertEquals(errors[i] ? null /* no result from server */ : Result.Updated.jsonValue(), bulkResponseItem.result()); } else if (bulkOperation.isDelete()) { assertEquals(errors[i] ? Result.NotFound.jsonValue() : Result.Deleted.jsonValue(), bulkResponseItem.result()); assertEquals(errors[i] ? 404 : 200, bulkResponseItem.status());