From 3b0c1c819da604126b9410795052f389daea5e2a Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Mar 2023 08:34:47 -0700 Subject: [PATCH 1/7] Functional Test Cases for Document DDB API and Surface API Review 2 comments --- .../dynamodb/document/EnhancedDocument.java | 98 ++- .../document/DefaultEnhancedDocument.java | 82 +- .../dynamodb/mapper/DocumentTableSchema.java | 7 +- .../document/EnhancedDocumentTest.java | 429 +++++++--- .../document/EnhancedDocumentTestData.java | 51 +- .../document/ParameterizedDocumentTest.java | 14 +- .../document/BasicAsyncCrudTest.java | 621 ++++++++++++++ .../document/BasicCrudTest.java | 611 ++++++++++++++ .../document/BasicQueryTest.java | 640 ++++++++++++++ .../document/BasicScanTest.java | 787 ++++++++++++++++++ .../document/IndexQueryTest.java | 261 ++++++ .../document/IndexScanTest.java | 239 ++++++ .../models/InnerAttribConverterProvider.java | 12 +- 13 files changed, 3658 insertions(+), 194 deletions(-) create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java create mode 100644 services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java index 0ade5fb9b495..59529c78912a 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -51,8 +51,8 @@ * {@snippet : * // CustomAttributeConverterProvider.create() is an example for some Custom converter provider * EnhancedDocument enhancedDocumentWithCustomConverter = EnhancedDocument.builder().attributeConverterProviders - * (CustomAttributeConverterProvider.create(), AttributeConverterProvide.defaultProvider()) - * .putWithType("customObject", customObject, EnhancedType.of(CustomClass.class)) + * (CustomAttributeConverterProvider.create(), AttributeConverterProvide.defaultProvider() + * .putWithTypethType("customObject", customObject, EnhancedType.of(CustomClass.class)) * .build(); *} *

Enhanced Document can be created with Json as input using Static factory method.In this case it used @@ -143,7 +143,7 @@ static Builder builder() { *

* Retrieving String Type for a document * {@snippet : - * Custom resultCustom = document.get("key", EnhancedType.of(Custom.class)); + * Custom resultCustom = document.get("key", EnhancedType.of(String.class)); * } * Retrieving Custom Type for which Convertor Provider was defined while creating the document * {@snippet : @@ -166,6 +166,31 @@ static Builder builder() { */ T get(String attributeName, EnhancedType type); + /** + * Returns the value of the specified attribute in the current document as a specified class type; or null if the + * attribute either doesn't exist or the attribute value is null. + *

+ * Retrieving String Type for a document + * {@snippet : + * Custom resultCustom = document.get("key", String.class); + * } + * Retrieving Custom Type for which Convertor Provider was defined while creating the document + * {@snippet : + * Custom resultCustom = document.get("key", Custom.class); + * } + *

+ * Note : + * This API should not be used to retrieve values of List and Map types. + * Instead, getList and getMap APIs should be used to retrieve attributes of type List and Map, respectively. + *

+ * @param attributeName Name of the attribute. + * @param clazz Class type of value. + * @param The type of the attribute value. + * @return Attribute value of type T + * } + */ + T get(String attributeName, Class clazz); + /** * Gets the String value of specified attribute in the document. * @@ -212,8 +237,8 @@ static Builder builder() { /** * Gets the Set of String values of the given attribute in the current document. * @param attributeName Name of the attribute. - * @return value of the specified attribute in the current document as a set of SdkBytes; or null if the attribute either - * doesn't exist or the attribute value is null. + * @return value of the specified attribute in the current document as a set of SdkBytes; + * or null if the attribute either doesn't exist. */ Set getBytesSet(String attributeName); @@ -223,8 +248,8 @@ static Builder builder() { * @param attributeName Name of the attribute. * @param type {@link EnhancedType} of Type T. * @param Type T of List elements - * @return value of the specified attribute in the current document as a list of type T; or null if the - * attribute either doesn't exist or the attribute value is null. + * @return value of the specified attribute in the current document as a list of type T, + * or null if the attribute does not exist. */ List getList(String attributeName, EnhancedType type); @@ -256,7 +281,7 @@ static Builder builder() { String getJson(String attributeName); /** - * Gets the {@link Boolean} value for the specified attribute. + * Gets the boolean value for the specified attribute. * * @param attributeName Name of the attribute. * @return value of the specified attribute in the current document as a non-null Boolean. @@ -264,7 +289,7 @@ static Builder builder() { * if either the attribute doesn't exist or if the attribute * value cannot be converted into a boolean value. */ - Boolean getBoolean(String attributeName); + boolean getBoolean(String attributeName); /** @@ -276,7 +301,7 @@ static Builder builder() { * @param attributeName Name of the attribute. * @return value of the specified attribute in the current document as a List of {@link AttributeValue} */ - List getUnknownTypeList(String attributeName); + List getListOfUnknownList(String attributeName); /** * Retrieves a Map with String keys and corresponding AttributeValue objects as values for a specified attribute in a @@ -287,7 +312,7 @@ static Builder builder() { * @param attributeName Name of the attribute. * @return value of the specified attribute in the current document as a {@link AttributeValue} */ - Map getUnknownTypeMap(String attributeName); + Map getMapOfUnknownType(String attributeName); /** * @@ -350,7 +375,7 @@ interface Builder { * @param value The boolean value that needs to be set. * @return Builder instance to construct a {@link EnhancedDocument} */ - Builder putBoolean(String attributeName, Boolean value); + Builder putBoolean(String attributeName, boolean value); /** * Appends an attribute of name attributeName with a null value. @@ -414,11 +439,33 @@ interface Builder { * provider. * Example: {@snippet : - EnhancedDocument.builder().putWithType("customKey", customValue, EnhancedType.of(CustomClass.class)); - * } + * EnhancedDocument.builder().put("customKey", customValue, EnhancedType.of(CustomClass.class)); + *} + * Use {@link #putString(String, String)} or {@link #putNumber(String, Number)} for inserting simple value types of + * attributes. + * Use {@link #putList(String, List, EnhancedType)} or {@link #putMap(String, Map, EnhancedType, EnhancedType)} for + * inserting collections of attribute values. + * Note that the attribute converter provider added to the DocumentBuilder must provide the converter for the class T + * that is to be inserted. + @param attributeName the name of the attribute to be added to the document. + @param value the value to set. + @param type the Enhanced type of the value to set. + @return a builder instance to construct a {@link EnhancedDocument}. + @param the type of the value to set. + */ + Builder put(String attributeName, T value, EnhancedType type); + + /** + * Appends an attribute named {@code attributeName} with a value of Class type T. + * Use this method to insert attribute values of custom types that have attribute converters defined in a converter + * provider. + * Example: + {@snippet : + * EnhancedDocument.builder().put("customKey", customValue, CustomClass.class); + *} * Use {@link #putString(String, String)} or {@link #putNumber(String, Number)} for inserting simple value types of * attributes. - * Use {@link #putList(String, List, EnhancedType)} or {@link #putMapOfType(String, Map, EnhancedType, EnhancedType)} for + * Use {@link #putList(String, List, EnhancedType)} or {@link #putMap(String, Map, EnhancedType, EnhancedType)} for * inserting collections of attribute values. * Note that the attribute converter provider added to the DocumentBuilder must provide the converter for the class T * that is to be inserted. @@ -428,7 +475,7 @@ interface Builder { @return a builder instance to construct a {@link EnhancedDocument}. @param the type of the value to set. */ - Builder putWithType(String attributeName, T value, EnhancedType type); + Builder put(String attributeName, T value, Class type); /** * Appends an attribute with the specified name and a Map containing keys and values of {@link EnhancedType} K @@ -437,14 +484,14 @@ interface Builder { * values. *

For example, to insert a map with String keys and Long values: * {@snippet : - * EnhancedDocument.builder().putMapOfType("stringMap", mapWithStringKeyNumberValue, EnhancedType.of(String.class), + * EnhancedDocument.builder().putMap("stringMap", mapWithStringKeyNumberValue, EnhancedType.of(String.class), * EnhancedType.of(String.class), EnhancedType.of(Long.class)) - * } + *} *

For example, to insert a map of String Key and Custom Values: * {@snippet : - * EnhancedDocument.builder().putMapOfType("customMap", mapWithStringKeyCustomValue, EnhancedType.of(String.class), + * EnhancedDocument.builder().putMap("customMap", mapWithStringKeyCustomValue, EnhancedType.of(String.class), * EnhancedType.of(String.class), EnhancedType.of(Custom.class)) - * } + *} * Note that the AttributeConverterProvider added to the DocumentBuilder should provide the converter for the classes * K and V that * are to be inserted. @@ -454,7 +501,7 @@ interface Builder { * @param valueType Enhanced type of Value class. * @return Builder instance to construct a {@link EnhancedDocument} */ - Builder putMapOfType(String attributeName, Map value, EnhancedType keyType, EnhancedType valueType); + Builder putMap(String attributeName, Map value, EnhancedType keyType, EnhancedType valueType); /** Appends an attribute to the document builder with the specified name and value of a JSON document in string format. @@ -464,6 +511,15 @@ interface Builder { */ Builder putJson(String attributeName, String json); + + /** + * Removes a previously appended attribute. + * This can be used where a previously added attribute to the Builder is no longer needed. + * @param attributeName The attribute that needs to be removed. + * @return Builder instance to construct a {@link EnhancedDocument} + */ + Builder remove(String attributeName); + /** * Appends collection of attributeConverterProvider to the document builder. These * AttributeConverterProvider will be used to convert any given key to custom type T. diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index beb233f197c0..5eebc8c81521 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -62,8 +62,10 @@ @SdkInternalApi public class DefaultEnhancedDocument implements EnhancedDocument { - public static final IllegalStateException NULL_SET_ERROR = new IllegalStateException("Set must not have null values."); + private static final IllegalStateException NULL_SET_ERROR = new IllegalStateException("Set must not have null values."); private static final JsonItemAttributeConverter JSON_ATTRIBUTE_CONVERTER = JsonItemAttributeConverter.create(); + private static final String VALIDATE_TYPE_ERROR = "Values of type %s are not supported by this API, please use the " + + "%s%s API instead"; private final Map nonAttributeValueMap; private final Map enhancedTypeMap; private final List attributeConverterProviders; @@ -140,7 +142,9 @@ public SdkNumber getNumber(String attributeName) { return get(attributeName, SdkNumber.class); } - private T get(String attributeName, Class clazz) { + @Override + public T get(String attributeName, Class clazz) { + checkAndValidateClass(clazz, false); return get(attributeName, EnhancedType.of(clazz)); } @@ -181,16 +185,21 @@ public String getJson(String attributeName) { if (attributeValue == null) { return null; } - return JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue).toString(); + return stringValue(JSON_ATTRIBUTE_CONVERTER.transformTo(attributeValue)); } @Override - public Boolean getBoolean(String attributeName) { - return get(attributeName, Boolean.class); + public boolean getBoolean(String attributeName) { + Boolean value = get(attributeName, Boolean.class); + if (value == null) { + throw new IllegalStateException("Value of " + "attribute " + attributeName + " of type null cannot be converted" + + " into a boolean value"); + } + return value.booleanValue(); } @Override - public List getUnknownTypeList(String attributeName) { + public List getListOfUnknownList(String attributeName) { AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); if (attributeValue == null) { return null; @@ -202,7 +211,7 @@ public List getUnknownTypeList(String attributeName) { } @Override - public Map getUnknownTypeMap(String attributeName) { + public Map getMapOfUnknownType(String attributeName) { AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); if (attributeValue == null) { return null; @@ -278,8 +287,18 @@ public DefaultBuilder(DefaultEnhancedDocument enhancedDocument) { } public Builder putObject(String attributeName, Object value) { - Validate.paramNotNull(attributeName, "attributeName"); - Validate.paramNotBlank(attributeName.trim(), "attributeName"); + putObject(attributeName, value, false); + return this; + } + + private Builder putObject(String attributeName, Object value, boolean ignoreNullValue) { + + if (!ignoreNullValue) { + checkInvalidAttribute(attributeName, value); + } else { + Validate.paramNotNull(attributeName, "attributeName"); + Validate.paramNotBlank(attributeName.trim(), "attributeName"); + } enhancedTypeMap.remove(attributeName); nonAttributeValueMap.remove(attributeName); nonAttributeValueMap.put(attributeName, value); @@ -302,13 +321,13 @@ public Builder putBytes(String attributeName, SdkBytes value) { } @Override - public Builder putBoolean(String attributeName, Boolean value) { - return putObject(attributeName, value); + public Builder putBoolean(String attributeName, boolean value) { + return putObject(attributeName, Boolean.valueOf(value)); } @Override public Builder putNull(String attributeName) { - return putObject(attributeName, null); + return putObject(attributeName, null, true); } @Override @@ -317,7 +336,7 @@ public Builder putStringSet(String attributeName, Set values) { if (values.stream().anyMatch(Objects::isNull)) { throw NULL_SET_ERROR; } - return putWithType(attributeName, values, EnhancedType.setOf(String.class)); + return put(attributeName, values, EnhancedType.setOf(String.class)); } @Override @@ -330,7 +349,7 @@ public Builder putNumberSet(String attributeName, Set values) { } return SdkNumber.fromString(number.toString()); }).collect(Collectors.toCollection(LinkedHashSet::new)); - return putWithType(attributeName, sdkNumberSet, EnhancedType.setOf(SdkNumber.class)); + return put(attributeName, sdkNumberSet, EnhancedType.setOf(SdkNumber.class)); } @Override @@ -339,18 +358,18 @@ public Builder putBytesSet(String attributeName, Set values) { if (values.stream().anyMatch(Objects::isNull)) { throw NULL_SET_ERROR; } - return putWithType(attributeName, values, EnhancedType.setOf(SdkBytes.class)); + return put(attributeName, values, EnhancedType.setOf(SdkBytes.class)); } @Override public Builder putList(String attributeName, List value, EnhancedType type) { checkInvalidAttribute(attributeName, value); Validate.paramNotNull(type, "type"); - return putWithType(attributeName, value, EnhancedType.listOf(type)); + return put(attributeName, value, EnhancedType.listOf(type)); } @Override - public Builder putWithType(String attributeName, T value, EnhancedType type) { + public Builder put(String attributeName, T value, EnhancedType type) { checkInvalidAttribute(attributeName, value); Validate.notNull(attributeName, "attributeName cannot be null."); enhancedTypeMap.put(attributeName, type); @@ -360,13 +379,20 @@ public Builder putWithType(String attributeName, T value, EnhancedType ty } @Override - public Builder putMapOfType(String attributeName, Map value, EnhancedType keyType, - EnhancedType valueType) { + public Builder put(String attributeName, T value, Class type) { + checkAndValidateClass(type, true); + put(attributeName, value, EnhancedType.of(type)); + return this; + } + + @Override + public Builder putMap(String attributeName, Map value, EnhancedType keyType, + EnhancedType valueType) { checkInvalidAttribute(attributeName, value); Validate.notNull(attributeName, "attributeName cannot be null."); Validate.paramNotNull(keyType, "keyType"); Validate.paramNotNull(valueType, "valueType"); - return putWithType(attributeName, value, EnhancedType.mapOf(keyType, valueType)); + return put(attributeName, value, EnhancedType.mapOf(keyType, valueType)); } @Override @@ -375,6 +401,12 @@ public Builder putJson(String attributeName, String json) { return putObject(attributeName, getAttributeValueFromJson(json)); } + @Override + public Builder remove(String attributeName) { + nonAttributeValueMap.remove(attributeName); + return this; + } + @Override public Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { Validate.paramNotNull(attributeConverterProvider, "attributeConverterProvider"); @@ -430,8 +462,9 @@ private static AttributeValue getAttributeValueFromJson(String json) { private static void checkInvalidAttribute(String attributeName, Object value) { Validate.paramNotNull(attributeName, "attributeName"); Validate.paramNotBlank(attributeName.trim(), "attributeName"); - Validate.notNull(value, "%s must not be null. Use putNull API to insert a Null value", value); + Validate.notNull(value, "Value for %s must not be null. Use putNull API to insert a Null value", attributeName); } + } @Override @@ -456,5 +489,12 @@ public int hashCode() { return result; } + private static void checkAndValidateClass(Class type, boolean isPut) { + Validate.paramNotNull(type, "type"); + Validate.isTrue(!type.isAssignableFrom(List.class), + String.format(VALIDATE_TYPE_ERROR, List.class.getSimpleName(), isPut ? "put" : "get", "List")); + Validate.isTrue(!type.isAssignableFrom(Map.class), + String.format(VALIDATE_TYPE_ERROR, Map.class.getSimpleName(), isPut ? "put" : "get", "Map")); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java index 13a4025fda91..52048894e3f4 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java @@ -19,8 +19,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -67,6 +69,7 @@ @SdkPublicApi public final class DocumentTableSchema implements TableSchema { + private static final AttributeValue NULL_ATTRIBUTE_VALUE = AttributeValue.fromNul(true); private final TableMetadata tableMetadata; private final List attributeConverterProviders; @@ -109,12 +112,12 @@ public Map itemToMap(EnhancedDocument item, boolean igno } private List mergeAttributeConverterProviders(EnhancedDocument item) { - List providers = new ArrayList<>(); + Set providers = new LinkedHashSet<>(); if (item.attributeConverterProviders() != null) { providers.addAll(item.attributeConverterProviders()); } providers.addAll(attributeConverterProviders); - return providers; + return providers.stream().collect(Collectors.toList()); } @Override diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java index 28a713550b7b..bd90919f831e 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -25,6 +25,7 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; @@ -34,6 +35,7 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -41,18 +43,32 @@ import org.junit.jupiter.params.provider.ValueSource; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomAttributeForDocumentConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; -class EnhancedDocumentTest{ +class EnhancedDocumentTest { + + private static Stream escapeDocumentStrings() { + char c = 0x0a; + return Stream.of( + Arguments.of(String.valueOf(c), "{\"key\":\"\\n\"}") + , Arguments.of("", "{\"key\":\"\"}") + , Arguments.of(" ", "{\"key\":\" \"}") + , Arguments.of("\t", "{\"key\":\"\\t\"}") + , Arguments.of("\n", "{\"key\":\"\\n\"}") + , Arguments.of("\r", "{\"key\":\"\\r\"}") + , Arguments.of("\f", "{\"key\":\"\\f\"}") + ); + } @Test void enhancedDocumentGetters() { EnhancedDocument document = testDataInstance() - .dataForScenario("complexDocWithSdkBytesAndMapArrays_And_PutOverWritten") - .getEnhancedDocument(); + .dataForScenario("complexDocWithSdkBytesAndMapArrays_And_PutOverWritten") + .getEnhancedDocument(); // Assert assertThat(document.getString("stringKey")).isEqualTo("stringValue"); assertThat(document.getNumber("numberKey")).isEqualTo(SdkNumber.fromInteger(1)); @@ -81,6 +97,64 @@ void enhancedDocumentGetters() { .containsExactlyEntriesOf(expectedUuidBigDecimalMap); } + @Test + public void enhancedDocWithNestedListAndMaps() { + /** + * No attributeConverters supplied, in this case it uses the {@link DefaultAttributeConverterProvider} and does not error + */ + EnhancedDocument simpleDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("HashKey", "abcdefg123") + .putNull("nullKey") + .putNumber("numberKey", 2.0) + .putBytes("sdkByte", SdkBytes.fromUtf8String("a")) + .putBoolean("booleanKey", true) + .putJson("jsonKey", "{\"1\": [\"a\", \"b\", \"c\"],\"2\": 1}") + .putStringSet("stingSet", + Stream.of("a", "b", "c").collect(Collectors.toSet())) + + .putNumberSet("numberSet", Stream.of(1, 2, 3, 4).collect(Collectors.toSet())) + .putBytesSet("sdkByteSet", + Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet())) + .build(); + + assertThat(simpleDoc.toJson()).isEqualTo("{\"HashKey\":\"abcdefg123\",\"nullKey\":null,\"numberKey\":2.0," + + "\"sdkByte\":\"a\",\"booleanKey\":true,\"jsonKey\":{\"1\":[\"a\",\"b\"," + + "\"c\"],\"2\":1},\"stingSet\":[\"a\",\"b\",\"c\"],\"numberSet\":[1,2,3,4]," + + "\"sdkByteSet\":[\"a\"]}"); + + + assertThat(simpleDoc.isPresent("HashKey")).isTrue(); + // No Null pointer or doesnot exist is thrown + assertThat(simpleDoc.isPresent("HashKey2")).isFalse(); + assertThat(simpleDoc.getString("HashKey")).isEqualTo("abcdefg123"); + assertThat(simpleDoc.isNull("nullKey")).isTrue(); + + assertThat(simpleDoc.getNumber("numberKey")).isNotEqualTo(2.0); + assertThat(simpleDoc.getNumber("numberKey").doubleValue()).isEqualTo(2.0); + + assertThat(simpleDoc.getBytes("sdkByte")).isEqualTo(SdkBytes.fromUtf8String("a")); + assertThat(simpleDoc.getBoolean("booleanKey")).isTrue(); + assertThat(simpleDoc.getJson("jsonKey")).isEqualTo("{\"1\":[\"a\",\"b\",\"c\"],\"2\":1}"); + assertThat(simpleDoc.getStringSet("stingSet")).isEqualTo(Stream.of("a", "b", "c").collect(Collectors.toSet())); + assertThat(simpleDoc.getList("stingSet", EnhancedType.of(String.class))).isEqualTo(Stream.of("a", "b", "c").collect(Collectors.toList())); + + assertThat(simpleDoc.getNumberSet("numberSet") + .stream().map(n -> n.intValue()).collect(Collectors.toSet())) + .isEqualTo(Stream.of(1, 2, 3, 4).collect(Collectors.toSet())); + + + assertThat(simpleDoc.getBytesSet("sdkByteSet")).isEqualTo(Stream.of(SdkBytes.fromUtf8String("a")).collect(Collectors.toSet())); + + + // Trying to access some other Types + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> simpleDoc.getBoolean("sdkByteSet")) + .withMessageContaining("BooleanAttributeConverter cannot convert " + + "an attribute of type BS into the requested type class java.lang.Boolean"); + + + } + @Test void testNullArgsInStaticConstructor() { assertThatNullPointerException() @@ -92,7 +166,6 @@ void testNullArgsInStaticConstructor() { .withMessage("json must not be null."); } - @Test void accessingSetFromBuilderMethodsAsListsInDocuments() { Set stringSet = Stream.of("a", "b", "c").collect(Collectors.toSet()); @@ -109,64 +182,63 @@ void accessingSetFromBuilderMethodsAsListsInDocuments() { assertThat(retrievedStringList).containsExactlyInAnyOrderElementsOf(stringSet); } + @Test + void builder_ResetsTheOldValues_beforeJsonSetterIsCalled() { + + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("simpleKeyOriginal", "simpleValueOld") + .json("{\"stringKey\": \"stringValue\"}") + .putString("simpleKeyNew", "simpleValueNew") + .build(); + + assertThat(enhancedDocument.toJson()).isEqualTo("{\"stringKey\":\"stringValue\",\"simpleKeyNew\":\"simpleValueNew\"}"); + assertThat(enhancedDocument.getString("simpleKeyOriginal")).isNull(); + + } + + @Test + void builder_with_NullKeys() { + String EMPTY_OR_NULL_ERROR = "attributeName must not be null."; + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putString(null, "Sample")) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putNull(null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putNumber(null, 3)) + .withMessage(EMPTY_OR_NULL_ERROR); - @Test - void builder_ResetsTheOldValues_beforeJsonSetterIsCalled() { - - EnhancedDocument enhancedDocument = EnhancedDocument.builder() - .attributeConverterProviders(defaultProvider()) - .putString("simpleKeyOriginal", "simpleValueOld") - .json("{\"stringKey\": \"stringValue\"}") - .putString("simpleKeyNew", "simpleValueNew") - .build(); - - assertThat(enhancedDocument.toJson()).isEqualTo("{\"stringKey\":\"stringValue\",\"simpleKeyNew\":\"simpleValueNew\"}"); - assertThat(enhancedDocument.getString("simpleKeyOriginal")).isNull(); - - } - - @Test - void builder_with_NullKeys() { - String EMPTY_OR_NULL_ERROR = "attributeName must not be null."; - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putString(null, "Sample")) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putNull(null)) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putNumber(null, 3)) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putList(null, Arrays.asList(), EnhancedType.of(String.class))) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putBytes(null, SdkBytes.fromUtf8String("a"))) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putMapOfType(null, new HashMap<>(), null, null)) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putNumberSet(null, Stream.of(1).collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - assertThatNullPointerException() - .isThrownBy(() -> EnhancedDocument.builder().putBytesSet(null, Stream.of(SdkBytes.fromUtf8String("a")) - .collect(Collectors.toSet()))) - .withMessage(EMPTY_OR_NULL_ERROR); - } + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putList(null, Arrays.asList(), EnhancedType.of(String.class))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putBytes(null, SdkBytes.fromUtf8String("a"))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putMap(null, new HashMap<>(), null, null)) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putNumberSet(null, Stream.of(1).collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putStringSet(null, Stream.of("a").collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + assertThatNullPointerException() + .isThrownBy(() -> EnhancedDocument.builder().putBytesSet(null, Stream.of(SdkBytes.fromUtf8String("a")) + .collect(Collectors.toSet()))) + .withMessage(EMPTY_OR_NULL_ERROR); + } @Test void errorWhen_NoAttributeConverter_IsProviderIsDefined() { @@ -178,24 +250,27 @@ void errorWhen_NoAttributeConverter_IsProviderIsDefined() { EnhancedType getType = EnhancedType.of(EnhancedDocumentTestData.class); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> enhancedDocument.get( - "stringKey",getType + "stringKey", getType )).withMessage( - "AttributeConverter not found for class EnhancedType(java.lang.String). Please add an AttributeConverterProvider for this type. " + "AttributeConverter not found for class EnhancedType(java.lang.String). Please add an AttributeConverterProvider " + + "for this type. " + "If it is a default type, add the DefaultAttributeConverterProvider to the builder."); } - @Test - void access_NumberAttributeFromMap() { - EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() - .dataForScenario("ElementsOfCustomType") - .getJson()); + @Test + void access_NumberAttributeFromMap() { + EnhancedDocument enhancedDocument = EnhancedDocument.fromJson(testDataInstance() + .dataForScenario("ElementsOfCustomType") + .getJson()); - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> - enhancedDocument.getNumber("customMapValue")) - .withMessage( - "software.amazon.awssdk.enhanced.dynamodb.internal.converter.attribute.SdkNumberAttributeConverter cannot convert" - + " an attribute of type M into the requested type class software.amazon.awssdk.core.SdkNumber"); - } + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> + enhancedDocument.getNumber("customMapValue")) + .withMessage( + "software.amazon.awssdk.enhanced.dynamodb.internal.converter" + + ".attribute.SdkNumberAttributeConverter cannot convert" + + " an attribute of type M into the requested type class " + + "software.amazon.awssdk.core.SdkNumber"); + } @Test void access_CustomType_without_AttributeConverterProvider() { @@ -208,62 +283,50 @@ void access_CustomType_without_AttributeConverterProvider() { assertThatExceptionOfType(IllegalStateException.class).isThrownBy( () -> enhancedDocument.get( - "customMapValue",enhancedType)).withMessage("Converter not found for " - + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters" - + ".document.CustomClassForDocumentAPI)"); + "customMapValue", enhancedType)).withMessage("Converter not found for " + + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters" + + ".document.CustomClassForDocumentAPI)"); EnhancedDocument docWithCustomProvider = enhancedDocument.toBuilder().attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create(), - defaultProvider()).build(); + defaultProvider()).build(); assertThat(docWithCustomProvider.get("customMapValue", EnhancedType.of(CustomClassForDocumentAPI.class))).isNotNull(); } - @Test - void error_When_DefaultProviderIsPlacedCustomProvider() { - CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") - .longNumber(26L) - .aBoolean(false).build(); - EnhancedDocument afterCustomClass = EnhancedDocument.builder() - .attributeConverterProviders( - - CustomAttributeForDocumentConverterProvider.create(), - defaultProvider()) - .putString("direct_attr", "sample_value") - .putWithType("customObject",customObject, - EnhancedType.of(CustomClassForDocumentAPI.class)) - .build(); - - assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\":\"sample_value\",\"customObject\":{\"longNumber\":26," - + "\"string\":\"str_one\"}}"); - - EnhancedDocument enhancedDocument = EnhancedDocument.builder() - .putString("direct_attr", "sample_value") - .putWithType("customObject", customObject, - EnhancedType.of(CustomClassForDocumentAPI.class)).attributeConverterProviders - (defaultProvider(), CustomAttributeForDocumentConverterProvider.create()) - .build(); + @Test + void error_When_DefaultProviderIsPlacedCustomProvider() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument afterCustomClass = EnhancedDocument.builder() + .attributeConverterProviders( + + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .put("customObject", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build(); - assertThatIllegalStateException().isThrownBy( - () -> enhancedDocument.toJson() - ).withMessage("Converter not found for " - + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); - } + assertThat(afterCustomClass.toJson()).isEqualTo("{\"direct_attr\":\"sample_value\",\"customObject\":{\"longNumber\":26," + + "\"string\":\"str_one\"}}"); - private static Stream escapeDocumentStrings() { - char c = 0x0a; - return Stream.of( - Arguments.of(String.valueOf(c),"{\"key\":\"\\n\"}") - , Arguments.of("","{\"key\":\"\"}") - , Arguments.of(" ","{\"key\":\" \"}") - , Arguments.of("\t","{\"key\":\"\\t\"}") - , Arguments.of("\n","{\"key\":\"\\n\"}") - , Arguments.of("\r","{\"key\":\"\\r\"}") - , Arguments.of("\f", "{\"key\":\"\\f\"}") - ); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .putString("direct_attr", "sample_value") + .put("customObject", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)).attributeConverterProviders + (defaultProvider(), + CustomAttributeForDocumentConverterProvider.create()) + .build(); + + assertThatIllegalStateException().isThrownBy( + () -> enhancedDocument.toJson() + ).withMessage("Converter not found for " + + "EnhancedType(software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI)"); } @ParameterizedTest - @ValueSource(strings = {"", " " , "\t", " ", "\n", "\r", "\f"}) - void invalidKeyNames(String escapingString){ + @ValueSource(strings = {"", " ", "\t", " ", "\n", "\r", "\f"}) + void invalidKeyNames(String escapingString) { assertThatIllegalArgumentException().isThrownBy(() -> EnhancedDocument.builder() .attributeConverterProviders(defaultProvider()) @@ -283,4 +346,130 @@ void escapingTheValues(String escapingString, String expectedJson) { .build(); assertThat(document.toJson()).isEqualTo(expectedJson); } + + + @Test + void removeParameterFromDocument() { + EnhancedDocument allSimpleTypes = testDataInstance().dataForScenario("allSimpleTypes").getEnhancedDocument(); + assertThat(allSimpleTypes.isPresent("nullKey")).isTrue(); + assertThat(allSimpleTypes.isNull("nullKey")).isTrue(); + assertThat(allSimpleTypes.getNumber("numberKey").intValue()).isEqualTo(10); + assertThat(allSimpleTypes.getString("stringKey")).isEqualTo("stringValue"); + + EnhancedDocument removedAttributesDoc = allSimpleTypes.toBuilder() + .remove("nullKey") + .remove("numberKey") + .build(); + + assertThat(removedAttributesDoc.isPresent("nullKey")).isFalse(); + assertThat(removedAttributesDoc.isNull("nullKey")).isFalse(); + assertThat(removedAttributesDoc.isPresent("numberKey")).isFalse(); + assertThat(removedAttributesDoc.getString("stringKey")).isEqualTo("stringValue"); + } + + @Test + void nullValueInsertion() { + + final String SAMPLE_KEY = "sampleKey"; + + String expectedNullMessage = "Value for sampleKey must not be null. Use putNull API to insert a Null value"; + + EnhancedDocument.Builder builder = EnhancedDocument.builder(); + assertThatNullPointerException().isThrownBy(() -> builder.putString(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.put(SAMPLE_KEY, null, + EnhancedType.of(String.class))).withMessageContaining(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putNumber(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putBytes(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putStringSet(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putBytesSet(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putJson(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putNumberSet(SAMPLE_KEY, null)).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putMap(SAMPLE_KEY, null, EnhancedType.of(String.class), + EnhancedType.of(String.class))).withMessage(expectedNullMessage); + assertThatNullPointerException().isThrownBy(() -> builder.putList(SAMPLE_KEY, null, EnhancedType.of(String.class))).withMessage(expectedNullMessage); + } + + + @Test + void accessingNulAttributeValue() { + String NULL_KEY = "nullKey"; + EnhancedDocument enhancedDocument = + EnhancedDocument.builder().attributeConverterProviders(defaultProvider()).putNull(NULL_KEY).build(); + + Assertions.assertNull(enhancedDocument.getString(NULL_KEY)); + Assertions.assertNull(enhancedDocument.getList(NULL_KEY, EnhancedType.of(String.class))); + assertThatIllegalStateException().isThrownBy(() -> enhancedDocument.getBoolean(NULL_KEY)) + .withMessage("Value of attribute nullKey of type null cannot be converted into a " + + "boolean value"); + + } + + @Test + void booleanValueRepresentation() { + EnhancedDocument.Builder builder = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()); + assertThat(builder.putString("boolean", "true").build().getBoolean("boolean")).isTrue(); + assertThat(builder.putNumber("boolean", 1).build().getBoolean("boolean")).isTrue(); + } + + + + @Test + void putAndGetOfCustomTypes_with_EnhancedTypeApi() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders( + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .put("customObject", customObject, + EnhancedType.of(CustomClassForDocumentAPI.class)) + .build(); + + assertThat(enhancedDocument.get("customObject", EnhancedType.of(CustomClassForDocumentAPI.class))) + .isEqualTo(customObject); + } + + @Test + void putAndGetOfCustomTypes_with_ClassTypes() { + CustomClassForDocumentAPI customObject = CustomClassForDocumentAPI.builder().string("str_one") + .longNumber(26L) + .aBoolean(false).build(); + EnhancedDocument enhancedDocument = EnhancedDocument.builder() + .attributeConverterProviders( + CustomAttributeForDocumentConverterProvider.create(), + defaultProvider()) + .putString("direct_attr", "sample_value") + .put("customObject", customObject, + CustomClassForDocumentAPI.class) + .build(); + + assertThat(enhancedDocument.get("customObject", CustomClassForDocumentAPI.class)).isEqualTo(customObject); + } + + + @Test + void error_when_usingClassGetPut_for_CollectionValues(){ + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().put("mapKey", new HashMap(), Map.class)) + .withMessage("Values of type Map are not supported by this API, please use the putMap API instead"); + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().put("listKey", new ArrayList<>() , List.class)) + .withMessage("Values of type List are not supported by this API, please use the putList API instead"); + + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().build().get("mapKey", Map.class)) + .withMessage("Values of type Map are not supported by this API, please use the getMap API instead"); + + assertThatIllegalArgumentException().isThrownBy( + () -> EnhancedDocument.builder().build().get("listKey" , List.class)) + .withMessage("Values of type List are not supported by this API, please use the getList API instead"); + + + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java index 6e9bfe2619ff..658c60755ded 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTestData.java @@ -26,7 +26,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -110,25 +109,25 @@ private void initializeTestData() { testDataList.add(dataBuilder().scenario("record") - .ddbItemMap(map().withKeyValue("id", AttributeValue.fromS("id-value")) - .withKeyValue("sort",AttributeValue.fromS("sort-value")) - .withKeyValue("attribute", AttributeValue.fromS("one")) - .withKeyValue("attribute2", AttributeValue.fromS("two")) - .withKeyValue("attribute3", AttributeValue.fromS("three")).get()) + .ddbItemMap(map().withKeyValue("uniqueId", AttributeValue.fromS("id-value")) + .withKeyValue("sortKey",AttributeValue.fromS("sort-value")) + .withKeyValue("attributeKey", AttributeValue.fromS("one")) + .withKeyValue("attributeKey2", AttributeValue.fromS("two")) + .withKeyValue("attributeKey3", AttributeValue.fromS("three")).get()) .enhancedDocument( defaultDocBuilder() - .putString("id","id-value") - .putString("sort","sort-value") - .putString("attribute","one") - .putString("attribute2","two") - .putString("attribute3","three") + .putString("uniqueId","id-value") + .putString("sortKey","sort-value") + .putString("attributeKey","one") + .putString("attributeKey2","two") + .putString("attributeKey3","three") .build() ) .attributeConverterProvider(defaultProvider()) - .json("{\"id\":\"id-value\",\"sort\":\"sort-value\",\"attribute\":\"one\"," - + "\"attribute2\":\"two\",\"attribute3\":\"three\"}") + .json("{\"uniqueId\":\"id-value\",\"sortKey\":\"sort-value\",\"attributeKey\":\"one\"," + + "\"attributeKey2\":\"two\",\"attributeKey3\":\"three\"}") .build()); @@ -333,8 +332,8 @@ private void initializeTestData() { .get()) .enhancedDocument( defaultDocBuilder() - .putMapOfType("simpleMap", getStringSimpleMap("suffix", 7, CharSequenceStringConverter.create()), - EnhancedType.of(CharSequence.class), EnhancedType.of(String.class)) + .putMap("simpleMap", getStringSimpleMap("suffix", 7, CharSequenceStringConverter.create()), + EnhancedType.of(CharSequence.class), EnhancedType.of(String.class)) .build() ) .attributeConverterProvider(defaultProvider()) @@ -360,13 +359,13 @@ private void initializeTestData() { defaultDocBuilder() .attributeConverterProviders(CustomAttributeForDocumentConverterProvider.create() , defaultProvider()) - .putMapOfType("customMapValue", - Stream.of(Pair.of("entryOne", customValueWithBaseAndOffset(2, 10))) + .putMap("customMapValue", + Stream.of(Pair.of("entryOne", customValueWithBaseAndOffset(2, 10))) .collect(Collectors.toMap(p -> CharSequenceStringConverter.create().fromString(p.left()), p -> p.right(), (oldValue, newValue) -> oldValue, LinkedHashMap::new)) , EnhancedType.of(CharSequence.class), - EnhancedType.of(CustomClassForDocumentAPI.class)) + EnhancedType.of(CustomClassForDocumentAPI.class)) .build() ) .json("{\"customMapValue\":{\"entryOne\":{\"instantList\":[\"2023-03-01T17:14:05.050Z\"," @@ -384,11 +383,11 @@ private void initializeTestData() { .ddbItemMap(map().withKeyValue("nullKey",AttributeValue.fromNul(true)).get()) .enhancedDocument( defaultDocBuilder() - .putString("nullKey", null) + .putNull("nullKey") .putNumber("numberKey", 1) .putString("stringKey", "stringValue") .putList("numberList", Arrays.asList(1, 2, 3), EnhancedType.of(Integer.class)) - .putWithType("simpleDate", LocalDate.MIN, EnhancedType.of(LocalDate.class)) + .put("simpleDate", LocalDate.MIN, EnhancedType.of(LocalDate.class)) .putStringSet("stringSet", Stream.of("one", "two").collect(Collectors.toSet())) .putBytes("sdkByteKey", SdkBytes.fromUtf8String("a")) .putBytesSet("sdkByteSet", @@ -396,16 +395,16 @@ private void initializeTestData() { SdkBytes.fromUtf8String("b")).collect(Collectors.toSet())) .putNumberSet("numberSetSet", Stream.of(1, 2).collect(Collectors.toSet())) .putList("numberList", Arrays.asList(4, 5, 6), EnhancedType.of(Integer.class)) - .putMapOfType("simpleMap", - mapFromSimpleKeyValue(Pair.of("78b3522c-2ab3-4162-8c5d" + .putMap("simpleMap", + mapFromSimpleKeyValue(Pair.of("78b3522c-2ab3-4162-8c5d" + "-f093fa76e68c", 3), Pair.of("4ae1f694-52ce-4cf6-8211" + "-232ccf780da8", 9)), - EnhancedType.of(String.class), EnhancedType.of(Integer.class)) - .putMapOfType("mapKey", mapFromSimpleKeyValue(Pair.of("1", Arrays.asList("a", "b" + EnhancedType.of(String.class), EnhancedType.of(Integer.class)) + .putMap("mapKey", mapFromSimpleKeyValue(Pair.of("1", Arrays.asList("a", "b" , "c")), Pair.of("2", Collections.singletonList("1"))), - EnhancedType.of(String.class), EnhancedType.listOf(String.class)) + EnhancedType.of(String.class), EnhancedType.listOf(String.class)) .build() ) @@ -520,7 +519,7 @@ private void initializeTestData() { SdkBytes.fromUtf8String("i3")) ,EnhancedType.of(SdkBytes.class) ) - .putMapOfType("mapOfBytes" + .putMap("mapOfBytes" , Stream.of(Pair.of("k1", SdkBytes.fromUtf8String("v1")) ,Pair.of("k2", SdkBytes.fromUtf8String("v2"))) .collect(Collectors.toMap(k->k.left(), diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java index 9816d46a95ff..cfe5f0c7ca5c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -101,15 +100,24 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { break; case S: assertThat(enhancedAttributeValue.asString()).isEqualTo(enhancedDocument.getString(key)); + assertThat(enhancedAttributeValue.asString()).isEqualTo(enhancedDocument.get(key, String.class)); + assertThat(enhancedAttributeValue.asString()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(String.class))); break; case N: assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.getNumber(key).stringValue()); + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.get(key, SdkNumber.class)); + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(SdkNumber.class))); + break; case B: assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.getBytes(key)); + assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.get(key, SdkBytes.class)); + assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(SdkBytes.class))); break; case BOOL: assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.getBoolean(key)); + assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.get(key, Boolean.class)); + assertThat(enhancedAttributeValue.asBoolean()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(Boolean.class))); break; case NS: Set expectedNumber = chainConverterProvider.converterFor(EnhancedType.setOf(SdkNumber.class)).transformTo(value); @@ -131,7 +139,7 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { throw new IllegalStateException("Converter not found for " + enhancedType); } assertThat(converter.transformTo(value)).isEqualTo(enhancedDocument.getList(key, enhancedType)); - assertThat(enhancedDocument.getUnknownTypeList(key)).isEqualTo(value.l()); + assertThat(enhancedDocument.getListOfUnknownList(key)).isEqualTo(value.l()); break; case M: EnhancedType keyType = enhancedTypeMap.get(key).get(0); @@ -142,7 +150,7 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { ); assertThat(mapAttributeConverter.transformTo(value)) .isEqualTo(enhancedDocument.getMap(key, keyType, valueType)); - assertThat(enhancedDocument.getUnknownTypeMap(key)).isEqualTo(value.m()); + assertThat(enhancedDocument.getMapOfUnknownType(key)).isEqualTo(value.m()); break; default: throw new IllegalStateException("EnhancedAttributeValue type not found: " + enhancedAttributeValue.type()); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java new file mode 100644 index 000000000000..4fbe5684b9bf --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java @@ -0,0 +1,621 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + + +@RunWith(Parameterized.class) +public class BasicAsyncCrudTest extends LocalDynamoDbAsyncTestBase { + + private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; + private final TestData testData; + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedAsyncClient enhancedClient; + private final String tableName = getConcreteTableName("table-name"); + private DynamoDbAsyncClient lowLevelClient; + + private DynamoDbAsyncTable docMappedtable ; + + + @Before + public void setUp(){ + lowLevelClient = getDynamoDbAsyncClient(); + enhancedClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable().join(); + + + + } + + public BasicAsyncCrudTest(TestData testData) { + this.testData = testData; + } + + @Parameterized.Parameters + public static Collection parameters() throws Exception { + return EnhancedDocumentTestData.testDataInstance().getAllGenericScenarios(); + } + + private static EnhancedDocument appendKeysToDoc(TestData testData) { + EnhancedDocument enhancedDocument = testData.getEnhancedDocument().toBuilder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + return enhancedDocument; + } + + private static Map simpleKey() { + Map key = new LinkedHashMap<>(); + key.put("id", AttributeValue.fromS("id-value")); + key.put("sort", AttributeValue.fromS("sort-value")); + return key; + } + + private static Map appendKeysToTestDataAttributeMap(Map attributeValueMap) { + + Map result = new LinkedHashMap<>(attributeValueMap); + result.put("id", AttributeValue.fromS("id-value")); + result.put("sort", AttributeValue.fromS("sort-value")); + return result; + } + + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()).join(); + } + + @Test + public void putThenGetItemUsingKey() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(enhancedDocument).join(); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(enhancedDocument.toMap()); + } + + @Test + public void putThenGetItemUsingKeyItem() throws ExecutionException, InterruptedException { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + + EnhancedDocument result = docMappedtable.getItem(EnhancedDocument.builder() + .attributeConverterProviders(testData.getAttributeConverterProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .build()).join(); + + Map attributeValueMap = appendKeysToTestDataAttributeMap(testData.getDdbItemMap()); + Assertions.assertThat(result.toMap()).isEqualTo(enhancedDocument.toMap()); + Assertions.assertThat(result.toMap()).isEqualTo(attributeValueMap); + } + + @Test + public void getNonExistentItem() { + EnhancedDocument item = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + Assertions.assertThat(item).isNull(); + } + + @Test + public void updateOverwriteCompleteItem_usingShortcutForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument).join(); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + + EnhancedDocument result = docMappedtable.updateItem(updateDocument).join(); + + Map updatedItemMap = new LinkedHashMap<>(testData.getDdbItemMap()); + + updatedItemMap.put("attribute", AttributeValue.fromS("four")); + updatedItemMap.put("attribute2", AttributeValue.fromS("five")); + updatedItemMap.put("attribute3", AttributeValue.fromS("six")); + updatedItemMap.put("id", AttributeValue.fromS("id-value")); + updatedItemMap.put("sort", AttributeValue.fromS("sort-value")); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(result.toMap()); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updatedItemMap); + } + + @Test + public void putTwiceThenGetItem() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument).join(); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + docMappedtable.putItem(r -> r.item(updateDocument)).join(); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + + // All the items are overwritten + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updateDocument.toMap()); + + EnhancedDocument docGetItem = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value" + ))).join(); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(docGetItem.toMap()); + + } + + @Test + public void putThenDeleteItem_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + Map key = simpleKey(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + + + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()).join(); + + + EnhancedDocument afterDeleteDoc = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()).join(); + + GetItemResponse lowLevelGetAfterDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + + + assertThat(enhancedDocument.toMap(), is(EnhancedDocument.fromAttributeValueMap(lowLevelGetBeforeDelete.item()).toMap())); + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(beforeDeleteResult.toMap(), is(lowLevelGetBeforeDelete.item())); + assertThat(afterDeleteDoc, is(nullValue())); + assertThat(lowLevelGetAfterDelete.item().size(), is(0)); + } + + @Test + public void putThenDeleteItem_usingKeyItemForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument); + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(enhancedDocument).join(); + EnhancedDocument afterDeleteResult = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()).join(); + + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(afterDeleteResult, is(nullValue())); + + Map key = simpleKey(); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); + assertThat(lowLevelGetBeforeDelete.item().size(), is(0)); + } + + @Test + public void putWithConditionThatSucceeds() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("one")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()).join(); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + assertThat(result.toMap(), is(newDoc.toMap())); + + + } + + @Test + public void putWithConditionThatFails() throws ExecutionException, InterruptedException { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()).join(); + } + + @Test + public void deleteNonExistentItem() { + EnhancedDocument result = + docMappedtable.deleteItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + Key key = docMappedtable.keyFrom(enhancedDocument); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()).join(); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(key)).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(docMappedtable.keyFrom(enhancedDocument)) + .conditionExpression(conditionExpression) + .build()).join(); + } + + @Test + public void updateOverwriteCompleteRecord_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(enhancedDocument); + // Updating new Items other than the one present in testData + EnhancedDocument.Builder updateDocBuilder = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "six"); + + EnhancedDocument expectedDocument = updateDocBuilder.build(); + + + // Explicitly Nullify each of the previous members + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + }); + + EnhancedDocument updateDocument = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(updateDocument).join(); + assertThat(result.toMap(), is(expectedDocument.toMap())); + assertThat(result.toJson(), is("{\"a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%\":\"six\",\"attribute\":\"four\"," + + "\"attribute2\":\"five\",\"id\":\"id-value\",\"sort\":\"sort-value\"}")); + } + + @Test + public void updateCreatePartialRecord() { + + EnhancedDocument.Builder docBuilder = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one"); + EnhancedDocument updateDoc = docBuilder.build(); + /** + * Explicitly removing AttributeNull Value that are added in testData for testing. + * This should not be treated as Null in partial update, because for a Document, an AttributeValue.fromNul(true) with a + * Null value is treated as Null or non-existent during updateItem. + */ + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + docBuilder.remove(entry.getKey()); + } + }); + EnhancedDocument expectedDocUpdate = docBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDoc)).join(); + assertThat(result.toMap(), is(expectedDocUpdate.toMap())); + } + + @Test + public void updateCreateKeyOnlyRecord() { + EnhancedDocument.Builder updateDocBuilder = appendKeysToDoc(testData).toBuilder(); + + EnhancedDocument expectedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + }); + + EnhancedDocument cleanedUpDoc = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(cleanedUpDoc)).join(); + assertThat(result.toMap(), is(expectedDocument.toMap())); + } + + @Test + public void updateOverwriteModelledNulls() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument updateDocument = EnhancedDocument.builder().attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putNull("attribute2") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS).build(); + + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDocument)).join(); + + + assertThat(result.isPresent("attribute2"), is(false)); + assertThat(result.isPresent(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS), is(false)); + assertThat(result.getString("attribute"), is("four")); + + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + assertThat(result.isPresent(entry.getKey()), is(true)); + } else { + assertThat(result.toMap().get(entry.getKey()), is(testData.getDdbItemMap().get(entry.getKey()))); + } + }); + } + + @Test + public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putNull("attribute") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .build(); + + + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(updateDocument) + .ignoreNulls(true) + .build()).join(); + + EnhancedDocument expectedResult = appendKeysToDoc(testData).toBuilder() + .putString("attribute2", "two") + .build(); + + assertThat(result.toMap(), is(expectedResult.toMap())); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + EnhancedDocument hashKeyAndSortOnly = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(hashKeyAndSortOnly) + .ignoreNulls(true) + .build()).join(); + assertThat(result.toMap(), is(enhancedDocument.toMap())); + } + + @Test + public void updateWithConditionThatSucceeds() { + + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()).join(); + + EnhancedDocument result = + docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); + assertThat(result.toMap(), is(enhancedDocument.toBuilder().putString("attribute", "four").build().toMap())); + } + + @Test + public void updateWithConditionThatFails() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()).join(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java new file mode 100644 index 000000000000..93c9b4fbb47d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -0,0 +1,611 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; +import software.amazon.awssdk.enhanced.dynamodb.document.TestData; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + + +@RunWith(Parameterized.class) +public class BasicCrudTest extends LocalDynamoDbSyncTestBase { + + private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; + private final TestData testData; + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("table-name"); + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + + + @Before + public void setUp(){ + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable(); + + + + } + + public BasicCrudTest(TestData testData) { + this.testData = testData; + } + + @Parameterized.Parameters + public static Collection parameters() throws Exception { + return EnhancedDocumentTestData.testDataInstance().getAllGenericScenarios(); + } + + private static EnhancedDocument appendKeysToDoc(TestData testData) { + EnhancedDocument enhancedDocument = testData.getEnhancedDocument().toBuilder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + return enhancedDocument; + } + + private static Map simpleKey() { + Map key = new LinkedHashMap<>(); + key.put("id", AttributeValue.fromS("id-value")); + key.put("sort", AttributeValue.fromS("sort-value")); + return key; + } + + private static Map appendKeysToTestDataAttributeMap(Map attributeValueMap) { + + Map result = new LinkedHashMap<>(attributeValueMap); + result.put("id", AttributeValue.fromS("id-value")); + result.put("sort", AttributeValue.fromS("sort-value")); + return result; + } + + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + } + + @Test + public void putThenGetItemUsingKey() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(enhancedDocument); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(enhancedDocument.toMap()); + } + + @Test + public void putThenGetItemUsingKeyItem() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument result = docMappedtable.getItem(EnhancedDocument.builder() + .attributeConverterProviders(testData.getAttributeConverterProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .build()); + + Map attributeValueMap = appendKeysToTestDataAttributeMap(testData.getDdbItemMap()); + Assertions.assertThat(result.toMap()).isEqualTo(enhancedDocument.toMap()); + Assertions.assertThat(result.toMap()).isEqualTo(attributeValueMap); + } + + @Test + public void getNonExistentItem() { + EnhancedDocument item = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + Assertions.assertThat(item).isNull(); + } + + @Test + public void updateOverwriteCompleteItem_usingShortcutForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + + EnhancedDocument result = docMappedtable.updateItem(updateDocument); + + Map updatedItemMap = new LinkedHashMap<>(testData.getDdbItemMap()); + + updatedItemMap.put("attribute", AttributeValue.fromS("four")); + updatedItemMap.put("attribute2", AttributeValue.fromS("five")); + updatedItemMap.put("attribute3", AttributeValue.fromS("six")); + updatedItemMap.put("id", AttributeValue.fromS("id-value")); + updatedItemMap.put("sort", AttributeValue.fromS("sort-value")); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(result.toMap()); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updatedItemMap); + } + + @Test + public void putTwiceThenGetItem() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument); + + // Updating new Items other than the one present in testData + EnhancedDocument updateDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString("attribute3", "six") + .build(); + docMappedtable.putItem(r -> r.item(updateDocument)); + Map key = simpleKey(); + GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + + // All the items are overwritten + Assertions.assertThat(lowLevelGet.item()).isEqualTo(updateDocument.toMap()); + + EnhancedDocument docGetItem = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value" + ))); + Assertions.assertThat(lowLevelGet.item()).isEqualTo(docGetItem.toMap()); + + } + + @Test + public void putThenDeleteItem_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + Map key = simpleKey(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + + + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + + EnhancedDocument afterDeleteDoc = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + GetItemResponse lowLevelGetAfterDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + + + assertThat(enhancedDocument.toMap(), is(EnhancedDocument.fromAttributeValueMap(lowLevelGetBeforeDelete.item()).toMap())); + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(beforeDeleteResult.toMap(), is(lowLevelGetBeforeDelete.item())); + assertThat(afterDeleteDoc, is(nullValue())); + assertThat(lowLevelGetAfterDelete.item().size(), is(0)); + } + + @Test + public void putThenDeleteItem_usingKeyItemForm() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(enhancedDocument); + EnhancedDocument beforeDeleteResult = + docMappedtable.deleteItem(enhancedDocument); + EnhancedDocument afterDeleteResult = + docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); + + assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); + assertThat(afterDeleteResult, is(nullValue())); + + Map key = simpleKey(); + GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); + assertThat(lowLevelGetBeforeDelete.item().size(), is(0)); + } + + @Test + public void putWithConditionThatSucceeds() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("one")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result.toMap(), is(newDoc.toMap())); + + + } + + @Test + public void putWithConditionThatFails() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString("attribute3", "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + + EnhancedDocument newDoc = enhancedDocument.toBuilder().putString("attribute", "four").build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + exception.expect(ConditionalCheckFailedException.class); + docMappedtable.putItem(PutItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression).build()); + } + + @Test + public void deleteNonExistentItem() { + EnhancedDocument result = + docMappedtable.deleteItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + Key key = docMappedtable.keyFrom(enhancedDocument); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(key)); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(docMappedtable.keyFrom(enhancedDocument)) + .conditionExpression(conditionExpression) + .build()); + } + + @Test + public void updateOverwriteCompleteRecord_usingShortcutForm() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(enhancedDocument); + // Updating new Items other than the one present in testData + EnhancedDocument.Builder updateDocBuilder = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putString("attribute2", "five") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "six"); + + EnhancedDocument expectedDocument = updateDocBuilder.build(); + + + // Explicitly Nullify each of the previous members + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + System.out.println(r); + }); + + EnhancedDocument updateDocument = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(updateDocument); + assertThat(result.toMap(), is(expectedDocument.toMap())); + assertThat(result.toJson(), is("{\"a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%\":\"six\",\"attribute\":\"four\"," + + "\"attribute2\":\"five\",\"id\":\"id-value\",\"sort\":\"sort-value\"}")); + } + + @Test + public void updateCreatePartialRecord() { + + EnhancedDocument.Builder docBuilder = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one"); + EnhancedDocument updateDoc = docBuilder.build(); + /** + * Explicitly removing AttributeNull Value that are added in testData for testing. + * This should not be treated as Null in partial update, because for a Document, an AttributeValue.fromNul(true) with a + * Null value is treated as Null or non-existent during updateItem. + */ + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + docBuilder.remove(entry.getKey()); + } + }); + EnhancedDocument expectedDocUpdate = docBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDoc)); + assertThat(result.toMap(), is(expectedDocUpdate.toMap())); + } + + @Test + public void updateCreateKeyOnlyRecord() { + EnhancedDocument.Builder updateDocBuilder = appendKeysToDoc(testData).toBuilder(); + + EnhancedDocument expectedDocument = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + + testData.getEnhancedDocument().toMap().keySet().forEach(r -> { + updateDocBuilder.putNull(r); + }); + + EnhancedDocument cleanedUpDoc = updateDocBuilder.build(); + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(cleanedUpDoc)); + assertThat(result.toMap(), is(expectedDocument.toMap())); + } + + @Test + public void updateOverwriteModelledNulls() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument updateDocument = EnhancedDocument.builder().attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .putNull("attribute2") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS).build(); + + EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDocument)); + + + assertThat(result.isPresent("attribute2"), is(false)); + assertThat(result.isPresent(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS), is(false)); + assertThat(result.getString("attribute"), is("four")); + + testData.getEnhancedDocument().toMap().entrySet().forEach(entry -> { + if (AttributeValue.fromNul(true).equals(entry.getValue())) { + assertThat(result.isPresent(entry.getKey()), is(true)); + } else { + assertThat(result.toMap().get(entry.getKey()), is(testData.getDdbItemMap().get(entry.getKey()))); + } + }); + } + + @Test + public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument updateDocument = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value") + .putNull("attribute") + .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .build(); + + + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(updateDocument) + .ignoreNulls(true) + .build()); + + EnhancedDocument expectedResult = appendKeysToDoc(testData).toBuilder() + .putString("attribute2", "two") + .build(); + + assertThat(result.toMap(), is(expectedResult.toMap())); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + EnhancedDocument enhancedDocument = appendKeysToDoc(testData); + docMappedtable.putItem(r -> r.item(enhancedDocument)); + EnhancedDocument hashKeyAndSortOnly = EnhancedDocument.builder() + .putString("id", "id-value") + .putString("sort", "sort-value").build(); + EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(hashKeyAndSortOnly) + .ignoreNulls(true) + .build()); + assertThat(result.toMap(), is(enhancedDocument.toMap())); + } + + @Test + public void updateWithConditionThatSucceeds() { + + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()); + + EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); + assertThat(result.toMap(), is(enhancedDocument.toBuilder().putString("attribute", "four").build().toMap())); + } + + @Test + public void updateWithConditionThatFails() { + + EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() + .putString("attribute", "one") + .putString("attribute2", "two") + .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") + .build(); + + docMappedtable.putItem(r -> r.item(enhancedDocument)); + + EnhancedDocument newDoc = EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putString("sort", "sort-value") + .putString("attribute", "four") + .build(); + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(ConditionalCheckFailedException.class); + docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) + .item(newDoc) + .conditionExpression(conditionExpression) + .build()); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java new file mode 100644 index 000000000000..45f4c11cd43b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java @@ -0,0 +1,640 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverter; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class BasicQueryTest extends LocalDynamoDbSyncTestBase { + + + + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + private DynamoDbTable neseteddocMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("doc-table-name"); + private final String nestedTableName = getConcreteTableName("doc-nested-table-name"); + + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable(); + + neseteddocMappedtable = enhancedClient.table(nestedTableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders( + new InnerAttribConverterProvider<>(), + defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "outerAttribOne", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .build()); + neseteddocMappedtable.createTable(); + + } + + @After + public void deleteTable() { + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(nestedTableName) + .build()); + } + + + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + private static final List DOCUMENTS_WITH_PROVIDERS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + public static EnhancedDocument createDocumentFromNestedRecord(NestedTestRecord nestedTestRecord){ + + EnhancedDocument.Builder enhancedDocument = + EnhancedDocument.builder(); + if (nestedTestRecord.getOuterAttribOne() != null) { + enhancedDocument.putString("outerAttribOne", nestedTestRecord.getOuterAttribOne()); + } + if (nestedTestRecord.getSort() != null) { + enhancedDocument.putNumber("sort", nestedTestRecord.getSort()); + } + if (nestedTestRecord.getDotVariable() != null) { + enhancedDocument.putString("test.com", nestedTestRecord.getDotVariable()); + } + InnerAttributeRecord innerAttributeRecord = nestedTestRecord.getInnerAttributeRecord(); + if (innerAttributeRecord != null) { + enhancedDocument.put("innerAttributeRecord", innerAttributeRecord, EnhancedType.of(InnerAttributeRecord.class)); + } + return enhancedDocument.build(); + + } + + private static final List NESTED_TEST_DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .map(BasicQueryTest::createDocumentFromNestedRecord) + + .collect(Collectors.toList()); + + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + + + + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + private void insertNestedDocuments() { + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + @Test + public void queryAllRecordsDefaultSettings_shortcutForm() { + insertDocuments(); + Iterator> results = + docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS.stream().map(i -> i + .toBuilder() + .attributeConverterProviders(new InnerAttribConverterProvider<>(), defaultProvider()) + .build() + .toJson()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + + } + + @Test + public void queryAllRecordsDefaultSettings_withProjection() { + insertDocuments(); + + Iterator> results = + docMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .attributesToProject("value") + ).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(DOCUMENTS.size())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("id"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.getNumber("value").intValue(), is(0)); + } + + @Test + public void queryAllRecordsDefaultSettings_shortcutForm_viaItems() { + insertDocuments(); + + PageIterable query = docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))); + SdkIterable results = query.items(); + + + assertThat(results.stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS.stream().map(i -> i + .toBuilder() + .attributeConverterProviders(new InnerAttribConverterProvider<>(), defaultProvider()) + .build() + .toJson()).collect(Collectors.toList()))); + + } + + @Test + public void queryAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#value >= :min_value AND #value <= :max_value") + .expressionValues(expressionValues) + .expressionNames(Collections.singletonMap("#value", "value")) + .build(); + + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .filterExpression(expression) + .build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map(doc -> doc.toJson()) + .collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryAllRecordsWithFilterAndProjection() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#value >= :min_value AND #value <= :max_value") + .expressionValues(expressionValues) + .expressionNames(Collections.singletonMap("#value", "value")) + .build(); + + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .filterExpression(expression) + .attributesToProject("value") + .build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items(), hasSize(3)); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + + EnhancedDocument record = page.items().get(0); + assertThat(record.getString("id"), nullValue()); + assertThat(record.getNumber("sort"), nullValue()); + assertThat(record.getNumber("value").intValue(), is(3)); + } + + @Test + public void queryBetween() { + insertDocuments(); + Key fromKey = Key.builder().partitionValue("id-value").sortValue(3).build(); + Key toKey = Key.builder().partitionValue("id-value").sortValue(5).build(); + Iterator> results = docMappedtable.query(r -> r.queryConditional(sortBetween(fromKey, toKey))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map(doc -> doc.toJson()) + .collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryLimit() { + insertDocuments(); + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .limit(5) + .build()) + .iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + Map expectedLastEvaluatedKey1 = new HashMap<>(); + expectedLastEvaluatedKey1.put("id", stringValue("id-value")); + expectedLastEvaluatedKey1.put("sort", numberValue(4)); + Map expectedLastEvaluatedKey2 = new HashMap<>(); + expectedLastEvaluatedKey2.put("id", stringValue("id-value")); + expectedLastEvaluatedKey2.put("sort", numberValue(9)); + assertThat(page1.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(0, 5).stream().map( doc -> doc.toJson()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(expectedLastEvaluatedKey1)); + assertThat(page2.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(5, 10).stream().map( doc -> doc.toJson()).collect(Collectors.toList()))); + assertThat(page2.lastEvaluatedKey(), is(expectedLastEvaluatedKey2)); + assertThat(page3.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryEmpty() { + Iterator> results = + docMappedtable.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("id-value")))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryEmpty_viaItems() { + PageIterable query = docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))); + SdkIterable results = query.items(); + assertThat(results.stream().collect(Collectors.toList()), is(empty())); + } + + @Test + public void queryExclusiveStartKey() { + Map exclusiveStartKey = new HashMap<>(); + exclusiveStartKey.put("id", stringValue("id-value")); + exclusiveStartKey.put("sort", numberValue(7)); + insertDocuments(); + Iterator> results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .exclusiveStartKey(exclusiveStartKey) + .build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(doc -> doc.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map(i -> i.toJson()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryExclusiveStartKey_viaItems() { + Map exclusiveStartKey = new HashMap<>(); + exclusiveStartKey.put("id", stringValue("id-value")); + exclusiveStartKey.put("sort", numberValue(7)); + insertDocuments(); + SdkIterable results = + docMappedtable.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) + .exclusiveStartKey(exclusiveStartKey) + .build()) + .items(); + + assertThat(results.stream().map(doc -> doc.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map(i -> i.toJson()).collect(Collectors.toList()))); + } + + @Test + public void queryNestedRecord_SingleAttributeName() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject(NestedAttributeName.builder().addElement("innerAttributeRecord") + .addElement("attribOne").build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getString("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is("attribOne-1")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(nullValue())); + results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addAttributeToProject("sort")).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(1)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + } + + @Test + public void queryNestedRecord_withAttributeNameList() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord", "attribOne").build(), + NestedAttributeName.builder().addElement("outerAttribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.builder() + .addElements(Arrays.asList("innerAttributeRecord", + "attribTwo")).build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-1")); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-1")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(1)); + } + + + @Test + public void queryNestedRecord_withAttributeNameListAndStringAttributeToProjectAppended() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributesToProject(Arrays.asList( + NestedAttributeName.builder().elements("innerAttributeRecord","attribOne").build())) + .addNestedAttributesToProject(NestedAttributeName.create("innerAttributeRecord","attribTwo")) + .addAttributeToProject("sort")).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(is(nullValue()))); + assertThat(firstRecord.getNumber("sort").intValue(), is(1)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-1")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(1)); + } + + + @Test + public void queryAllRecordsDefaultSettings_withNestedProjectionNamesNotInNameMap() { + insertNestedDocuments(); + + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) + .addNestedAttributeToProject( NestedAttributeName.builder().addElement("nonExistentSlot").build())).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord, is(nullValue())); + } + + @Test + public void queryRecordDefaultSettings_withDotInTheName() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .addNestedAttributeToProject( NestedAttributeName.create("test.com"))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(is(nullValue()))); + assertThat(firstRecord.getNumber("sort"), is(is(nullValue()))); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)) , is(nullValue())); + assertThat(firstRecord.getString("test.com"), is("v7")); + Iterator> resultWithAttributeToProject = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject( "test.com").build()).iterator(); + assertThat(resultWithAttributeToProject.hasNext(), is(true)); + Page pageResult = resultWithAttributeToProject.next(); + assertThat(resultWithAttributeToProject.hasNext(), is(false)); + assertThat(pageResult.items().size(), is(1)); + EnhancedDocument record = pageResult.items().get(0); + assertThat(record.getString("outerAttribOne"), is(is(nullValue()))); + assertThat(record.getNumber("sort"), is(is(nullValue()))); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)) , is(nullValue())); + assertThat(record.getString("test.com"), is("v7")); + } + + + @Test + public void queryRecordDefaultSettings_withEmptyAttributeList() { + insertNestedDocuments(); + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(new ArrayList<>()).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-7")); + assertThat(firstRecord.getNumber("sort").intValue(), is(7)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(7)); + assertThat(firstRecord.getString("test.com"), is("v7")); + } + + + @Test + public void queryRecordDefaultSettings_withNullAttributeList() { + insertNestedDocuments(); + + List backwardCompatibilty = null; + + Iterator> results = + neseteddocMappedtable.query(b -> b + .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) + .attributesToProject(backwardCompatibilty).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(1)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-7")); + assertThat(firstRecord.getNumber("sort").intValue(), is(7)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(7)); + assertThat(firstRecord.getString("test.com"), is("v7")); + } + + @Test + public void queryAllRecordsDefaultSettings_withNestedProjectionNameEmptyNameMap() { + insertNestedDocuments(); + + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> { + Iterator> results = neseteddocMappedtable.query(b -> b.queryConditional( + keyEqualTo(k -> k.partitionValue("id-value-3"))) + .attributesToProject("").build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + }); + + assertThatExceptionOfType(Exception.class).isThrownBy( + () -> { + Iterator> results = neseteddocMappedtable.query(b -> b.queryConditional( + keyEqualTo(k -> k.partitionValue("id-value-3"))) + .addNestedAttributeToProject(NestedAttributeName.create("")).build()).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + + }); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java new file mode 100644 index 000000000000..97410a2f753d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java @@ -0,0 +1,787 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.NestedAttributeName; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class BasicScanTest extends LocalDynamoDbSyncTestBase { + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + private DynamoDbTable neseteddocMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("doc-table-name"); + private final String nestedTableName = getConcreteTableName("doc-nested-table-name"); + + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + + mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + mappedNestedTable = enhancedClient.table(getConcreteTableName("nested-table-name"), + TableSchema.fromClass(NestedTestRecord.class)); + + mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + mappedNestedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); + + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + docMappedtable.createTable(); + + neseteddocMappedtable = enhancedClient.table(nestedTableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders( + new InnerAttribConverterProvider<>(), + defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "outerAttribOne", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .build()); + neseteddocMappedtable.createTable(); + + } + + + private static class Record { + private String id; + private Integer sort; + + private String getId() { + return id; + } + + private Record setId(String id) { + this.id = id; + return this; + } + + private Integer getSort() { + return sort; + } + + private Record setSort(Integer sort) { + this.sort = sort; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort); + } + } + + private static final TableSchema TABLE_SCHEMA = + StaticTableSchema.builder(Record.class) + .newItemSupplier(Record::new) + .addAttribute(String.class, a -> a.name("id") + .getter(Record::getId) + .setter(Record::setId) + .tags(primaryPartitionKey())) + .addAttribute(Integer.class, a -> a.name("sort") + .getter(Record::getSort) + .setter(Record::setSort) + .tags(primarySortKey())) + .build(); + + private static final List RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> new Record().setId("id-value").setSort(i)) + .collect(Collectors.toList()); + + private static final List NESTED_TEST_RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .collect(Collectors.toList()); + + + private DynamoDbTable mappedTable; + + private DynamoDbTable mappedNestedTable; + + + private void insertRecords() { + RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record))); + } + + private void insertNestedRecords() { + NESTED_TEST_RECORDS.forEach(nestedTestRecord -> mappedNestedTable.putItem(r -> r.item(nestedTestRecord))); + } + + + + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + private static final List DOCUMENTS_WITH_PROVIDERS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .build() + + ).collect(Collectors.toList()); + + + private static final List NESTED_TEST_DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> { + final NestedTestRecord nestedTestRecord = new NestedTestRecord(); + nestedTestRecord.setOuterAttribOne("id-value-" + i); + nestedTestRecord.setSort(i); + final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); + innerAttributeRecord.setAttribOne("attribOne-"+i); + innerAttributeRecord.setAttribTwo(i); + nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); + nestedTestRecord.setDotVariable("v"+i); + return nestedTestRecord; + }) + .map(BasicQueryTest::createDocumentFromNestedRecord) + + .collect(Collectors.toList()); + + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + private void insertNestedDocuments() { + NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); + } + + @After + public void deleteTable() { + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(nestedTableName) + .build()); + } + + + + @Test + public void scanAllRecordsDefaultSettings() { + insertDocuments(); + + docMappedtable.scan(ScanEnhancedRequest.builder().build()) + .forEach(p -> p.items().forEach(item -> System.out.println(item))); + Iterator> results = docMappedtable.scan(ScanEnhancedRequest.builder().build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(doc -> doc.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().map(i -> i.toJson()).collect(Collectors.toList()))); + + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + + @Test + public void queryAllRecordsDefaultSettings_withProjection() { + insertDocuments(); + + Iterator> results = + docMappedtable.scan(b -> b.attributesToProject("sort")).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().size(), is(RECORDS.size())); + + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("id"), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(0)); + } + + + + @Test + public void scanAllRecordsDefaultSettings_viaItems() { + insertDocuments(); + SdkIterable items = docMappedtable.scan(ScanEnhancedRequest.builder().limit(2).build()).items(); + assertThat(items.stream().map(i->i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().map(i -> i.toJson()).collect(Collectors.toList()))); + } + + + + + + + + + + @Test + public void scanAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("sort >= :min_value AND sort <= :max_value") + .expressionValues(expressionValues) + .build(); + + Iterator> results = + docMappedtable.scan(ScanEnhancedRequest.builder().filterExpression(expression).build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map( j -> j.toJson()) + .collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilterAndProjection() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + docMappedtable.scan( + ScanEnhancedRequest.builder() + .attributesToProject("sort") + .filterExpression(expression) + .build() + ).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items(), hasSize(3)); + + EnhancedDocument record = page.items().get(0); + + assertThat(record.getString("id"), is(nullValue())); + assertThat(record.getNumber("sort").intValue(), is(3)); + } + + @Test + public void scanLimit() { + insertDocuments(); + Iterator> results = docMappedtable.scan(r -> r.limit(5)).iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page1.items().stream().map( i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(0, 5).stream().map( i -> i.toJson()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4))); + assertThat(page2.items().stream().map( i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(5, 10).stream().map( i -> i.toJson()).collect(Collectors.toList()))); + + assertThat(page2.lastEvaluatedKey(), is(getKeyMap(9))); + assertThat(page3.items(), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + + @Test + public void scanLimit_viaItems() { + insertDocuments(); + SdkIterable results = docMappedtable.scan(r -> r.limit(5)).items(); + assertThat(results.stream().map(i -> i.toJson()) + .collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.stream().map(i ->i.toJson()).collect(Collectors.toList()))); + } + + @Test + public void scanEmpty() { + Iterator> results = docMappedtable.scan().iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanEmpty_viaItems() { + Iterator results = docMappedtable.scan().items().iterator(); + assertThat(results.hasNext(), is(false)); + } + + @Test + public void scanExclusiveStartKey() { + insertDocuments(); + Iterator> results = + docMappedtable.scan(r -> r.exclusiveStartKey(getKeyMap(7))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map( i -> i.toJson()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + + @Test + public void scanExclusiveStartKey_viaItems() { + insertDocuments(); + SdkIterable results = + docMappedtable.scan(r -> r.exclusiveStartKey(getKeyMap(7))).items(); + assertThat(results.stream().map( i-> i.toJson()).collect(Collectors.toList()), + is(DOCUMENTS_WITH_PROVIDERS.subList(8, 10).stream().map( i-> i.toJson()).collect(Collectors.toList()))); + } + + private Map getKeyMap(int sort) { + Map result = new HashMap<>(); + result.put("id", stringValue("id-value")); + result.put("sort", numberValue(sort)); + return Collections.unmodifiableMap(result); + } + @Test + public void scanAllRecordsWithFilterAndNestedProjectionSingleAttribute() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord","attribOne"))) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne() + .compareTo(item2.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), + is(nullValue())); + + //Attribute repeated with new and old attributeToProject + results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.create("sort")) + .addAttributeToProject("sort") + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + firstRecord = page.items().get(0); + assertThat(firstRecord.get("outerAttribOne", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + + results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributeToProject( + NestedAttributeName.create(Arrays.asList("innerAttributeRecord","attribOne"))) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne() + .compareTo(item2.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne())); + firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), + is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilterAndNestedProjectionMultipleAttribute() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + final ScanEnhancedRequest build = ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("outerAttribOne") + .addNestedAttributesToProject(Arrays.asList(NestedAttributeName.builder().elements("innerAttributeRecord") + .addElement("attribOne").build())) + .addNestedAttributeToProject(NestedAttributeName.builder() + .elements(Arrays.asList("innerAttributeRecord", "attribTwo")).build()) + .build(); + Iterator> results = + neseteddocMappedtable.scan( + build + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne() + .compareTo(item2.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-3")); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(3)); + } + + @Test + public void scanAllRecordsWithNonExistigKeyName() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + + Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().addElement("nonExistent").build()) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord, is(nullValue())); + } + + @Test + public void scanAllRecordsWithDotInAttributeKeyName() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName + .create("test.com")).build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getString("test.com") + .compareTo(item2.getString("test.com"))); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("uterAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort"), is(nullValue())); + assertThat(firstRecord.getString("test.com"), is("v3")); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + } + + @Test + public void scanAllRecordsWithSameNamesRepeated() { + //Attribute repeated with new and old attributeToProject + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().elements("sort").build()) + .addAttributeToProject("sort") + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is(nullValue())); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)), is(nullValue())); + } + + @Test + public void scanAllRecordsWithEmptyList() { + //Attribute repeated with new and old attributeToProject + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(new ArrayList<>()) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-3")); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is("attribOne-3")); + } + + @Test + public void scanAllRecordsWithNullAttributesToProject() { + //Attribute repeated with new and old attributeToProject + insertNestedDocuments(); + List backwardCompatibilityNull = null; + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + Iterator >results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .attributesToProject("test.com") + .attributesToProject(backwardCompatibilityNull) + .build() + ).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().size(), is(3)); + Collections.sort(page.items(), (item1, item2) -> + item1.getNumber("sort").bigDecimalValue() + .compareTo(item2.getNumber("sort").bigDecimalValue())); + EnhancedDocument firstRecord = page.items().get(0); + assertThat(firstRecord.getString("outerAttribOne"), is("id-value-3")); + assertThat(firstRecord.getNumber("sort").intValue(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(3)); + assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribOne(), is( + "attribOne-3")); + } + + @Test + public void scanAllRecordsWithNestedProjectionNameEmptyNameMap() { + insertNestedDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("#sort >= :min_value AND #sort <= :max_value") + .expressionValues(expressionValues) + .putExpressionName("#sort", "sort") + .build(); + + final Iterator> results = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addNestedAttributesToProject(NestedAttributeName.builder().elements("").build()).build() + ).iterator(); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { final boolean b = results.hasNext(); + Page next = results.next(); }).withMessageContaining("ExpressionAttributeNames contains invalid " + + "value"); + + final Iterator> resultsAttributeToProject = + neseteddocMappedtable.scan( + ScanEnhancedRequest.builder() + .filterExpression(expression) + .addAttributeToProject("").build() + ).iterator(); + + assertThatExceptionOfType(Exception.class).isThrownBy(() -> { + final boolean b = resultsAttributeToProject.hasNext(); + Page next = resultsAttributeToProject.next(); + }); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java new file mode 100644 index 000000000000..c5f8bbf1506c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java @@ -0,0 +1,261 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbIndex; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class IndexQueryTest extends LocalDynamoDbSyncTestBase { + + + + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("doc-table-name"); + DynamoDbIndex keysOnlyMappedIndex ; + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .addIndexPartitionKey("gsi_keys_only", "gsi_id", AttributeValueType.S) + .addIndexSortKey("gsi_keys_only", "gsi_sort", AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + + docMappedtable.createTable(CreateTableEnhancedRequest.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_keys_only") + .projection(p -> p.projectionType(ProjectionType.KEYS_ONLY)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build()) + .build()); + + keysOnlyMappedIndex = docMappedtable.index("gsi_keys_only"); + + } + + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .putString("gsi_id", "gsi-id-value") + .putNumber("gsi_sort", i) + .build() + ).collect(Collectors.toList()); + + private static final List KEYS_ONLY_DOCUMENTS = + DOCUMENTS.stream() + .map(record -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", record.getString("id")) + .putNumber("sort", record.getNumber("sort")) + .putString("gsi_id", record.getString("gsi_id")) + .putNumber("gsi_sort", record.getNumber("gsi_sort")).build() + ) + .collect(Collectors.toList()); + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + } + + + + @After + public void deleteTable() { + + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + + + } + + + @Test + public void queryAllRecordsDefaultSettings_usingShortcutForm() { + insertDocuments(); + + Iterator> results = + keysOnlyMappedIndex.query(keyEqualTo(k -> k.partitionValue("gsi-id-value"))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryBetween() { + insertDocuments(); + Key fromKey = Key.builder().partitionValue("gsi-id-value").sortValue(3).build(); + Key toKey = Key.builder().partitionValue("gsi-id-value").sortValue(5).build(); + Iterator> results = + keysOnlyMappedIndex.query(r -> r.queryConditional(QueryConditional.sortBetween(fromKey, toKey))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 && r.getNumber("sort").intValue() <= 5) + .map( j -> j.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + + @Test + public void queryLimit() { + insertDocuments(); + Iterator> results = + keysOnlyMappedIndex.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value"))) + .limit(5) + .build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + Map expectedLastEvaluatedKey1 = new HashMap<>(); + expectedLastEvaluatedKey1.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(4).getString("id"))); + expectedLastEvaluatedKey1.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(4).getNumber("sort"))); + expectedLastEvaluatedKey1.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(4).getString("gsi_id"))); + expectedLastEvaluatedKey1.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(4).getNumber("gsi_sort"))); + Map expectedLastEvaluatedKey2 = new HashMap<>(); + expectedLastEvaluatedKey2.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(9).getString("id"))); + expectedLastEvaluatedKey2.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(9).getNumber("sort"))); + expectedLastEvaluatedKey2.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(9).getString("gsi_id"))); + expectedLastEvaluatedKey2.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(9).getNumber("gsi_sort"))); + + assertThat(page1.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(0, 5).stream().map( i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(expectedLastEvaluatedKey1)); + assertThat(page2.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(KEYS_ONLY_DOCUMENTS.subList(5, 10).stream().map( i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page2.lastEvaluatedKey(), is(expectedLastEvaluatedKey2)); + assertThat(page3.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + + @Test + public void queryEmpty() { + Iterator> results = + keysOnlyMappedIndex.query(r -> r.queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value")))).iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryExclusiveStartKey() { + insertDocuments(); + Map expectedLastEvaluatedKey = new HashMap<>(); + expectedLastEvaluatedKey.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(7).getString("id"))); + expectedLastEvaluatedKey.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(7).getNumber("sort"))); + expectedLastEvaluatedKey.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(7).getString("gsi_id"))); + expectedLastEvaluatedKey.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(7).getNumber("gsi_sort"))); + Iterator> results = + keysOnlyMappedIndex.query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue("gsi-id-value"))) + .exclusiveStartKey(expectedLastEvaluatedKey).build()) + .iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(8, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java new file mode 100644 index 000000000000..7ac2559fb939 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java @@ -0,0 +1,239 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.document; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbIndex; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class IndexScanTest extends LocalDynamoDbSyncTestBase { + + + + private DynamoDbClient lowLevelClient; + + private DynamoDbTable docMappedtable ; + + @Rule + public ExpectedException exception = ExpectedException.none(); + private DynamoDbEnhancedClient enhancedClient; + private final String tableName = getConcreteTableName("table-name"); + DynamoDbIndex keysOnlyMappedIndex ; + + @Before + public void createTable() { + + + lowLevelClient = getDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + + docMappedtable = enhancedClient.table(tableName, + TableSchema.documentSchemaBuilder() + .attributeConverterProviders(defaultProvider()) + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + "id", + AttributeValueType.S) + .addIndexSortKey(TableMetadata.primaryIndexName(), "sort", + AttributeValueType.N) + .addIndexPartitionKey("gsi_keys_only", "gsi_id", AttributeValueType.S) + .addIndexSortKey("gsi_keys_only", "gsi_sort", AttributeValueType.N) + .attributeConverterProviders(defaultProvider()) + .build()); + + docMappedtable.createTable(CreateTableEnhancedRequest.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_keys_only") + .projection(p -> p.projectionType(ProjectionType.KEYS_ONLY)) + .provisionedThroughput(getDefaultProvisionedThroughput()) + .build()) + .build()); + keysOnlyMappedIndex = docMappedtable.index("gsi_keys_only"); + } + + private static final List DOCUMENTS = + IntStream.range(0, 10) + .mapToObj(i -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", "id-value") + .putNumber("sort", i) + .putNumber("value", i) + .putString("gsi_id", "gsi-id-value") + .putNumber("gsi_sort", i) + .build() + ).collect(Collectors.toList()); + + private static final List KEYS_ONLY_DOCUMENTS = + DOCUMENTS.stream() + .map(record -> EnhancedDocument.builder() + .attributeConverterProviders(defaultProvider()) + .putString("id", record.getString("id")) + .putNumber("sort", record.getNumber("sort")) + .putString("gsi_id", record.getString("gsi_id")) + .putNumber("gsi_sort", record.getNumber("gsi_sort")).build() + ) + .collect(Collectors.toList()); + + private void insertDocuments() { + DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); + } + + + @After + public void deleteTable() { + getDynamoDbClient().deleteTable(DeleteTableRequest.builder() + .tableName(tableName) + .build()); + } + + @Test + public void scanAllRecordsDefaultSettings() { + insertDocuments(); + + Iterator> results = keysOnlyMappedIndex.scan(ScanEnhancedRequest.builder().build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilter() { + insertDocuments(); + Map expressionValues = new HashMap<>(); + expressionValues.put(":min_value", numberValue(3)); + expressionValues.put(":max_value", numberValue(5)); + Expression expression = Expression.builder() + .expression("sort >= :min_value AND sort <= :max_value") + .expressionValues(expressionValues) + .build(); + + Iterator> results = + keysOnlyMappedIndex.scan(ScanEnhancedRequest.builder().filterExpression(expression).build()).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.stream().filter(r -> r.getNumber("sort").intValue() >= 3 + && r.getNumber("sort").intValue() <= 5).map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanLimit() { + insertDocuments(); + Iterator> results = keysOnlyMappedIndex.scan(r -> r.limit(5)).iterator(); + assertThat(results.hasNext(), is(true)); + Page page1 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page2 = results.next(); + assertThat(results.hasNext(), is(true)); + Page page3 = results.next(); + assertThat(results.hasNext(), is(false)); + + + + assertThat(page1.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(0, 5).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4))); + assertThat(page2.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(KEYS_ONLY_DOCUMENTS.subList(5, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page2.lastEvaluatedKey(), is(getKeyMap(9))); + assertThat(page3.items(), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanEmpty() { + Iterator> results = keysOnlyMappedIndex.scan().iterator(); + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanExclusiveStartKey() { + insertDocuments(); + Iterator> results = + keysOnlyMappedIndex.scan(r -> r.exclusiveStartKey(getKeyMap(7))).iterator(); + + assertThat(results.hasNext(), is(true)); + Page page = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), + is(KEYS_ONLY_DOCUMENTS.subList(8, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + private Map getKeyMap(int sort) { + Map result = new HashMap<>(); + result.put("id", stringValue(KEYS_ONLY_DOCUMENTS.get(sort).getString("id"))); + result.put("sort", numberValue(KEYS_ONLY_DOCUMENTS.get(sort).getNumber("sort"))); + result.put("gsi_id", stringValue(KEYS_ONLY_DOCUMENTS.get(sort).getString("gsi_id"))); + result.put("gsi_sort", numberValue(KEYS_ONLY_DOCUMENTS.get(sort).getNumber("gsi_sort"))); + return Collections.unmodifiableMap(result); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java index b38cc7554a37..ac28ff64f188 100755 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/InnerAttribConverterProvider.java @@ -1,9 +1,14 @@ package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; +import java.util.Map; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter; import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAPI; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomClassForDocumentAttributeConverter; +import software.amazon.awssdk.enhanced.dynamodb.converters.document.CustomIntegerAttributeConverter; +import software.amazon.awssdk.utils.ImmutableMap; /** * InnerAttribConverterProvider to save the InnerAttribConverter on the class. @@ -11,8 +16,13 @@ public class InnerAttribConverterProvider implements AttributeConverterProvider { + private final Map, AttributeConverter> converterCache = ImmutableMap.of( + EnhancedType.of(InnerAttributeRecord.class), new InnerAttribConverter() + ); + + @Override public AttributeConverter converterFor(EnhancedType enhancedType) { - return new InnerAttribConverter(); + return (AttributeConverter) converterCache.get(enhancedType); } } \ No newline at end of file From 17be3a56fbbbfb3486545ccb3b8a0d7637e525c4 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Mar 2023 09:16:04 -0700 Subject: [PATCH 2/7] Removed extra newlines from test cases --- .../dynamodb/document/EnhancedDocument.java | 2 +- .../document/BasicAsyncCrudTest.java | 27 +--- .../document/BasicCrudTest.java | 33 +---- .../document/BasicQueryTest.java | 31 ----- .../document/BasicScanTest.java | 120 +----------------- .../document/IndexQueryTest.java | 17 --- .../document/IndexScanTest.java | 13 -- 7 files changed, 5 insertions(+), 238 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java index 59529c78912a..fbd51ef47690 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -52,7 +52,7 @@ * // CustomAttributeConverterProvider.create() is an example for some Custom converter provider * EnhancedDocument enhancedDocumentWithCustomConverter = EnhancedDocument.builder().attributeConverterProviders * (CustomAttributeConverterProvider.create(), AttributeConverterProvide.defaultProvider() - * .putWithTypethType("customObject", customObject, EnhancedType.of(CustomClass.class)) + * .put("customObject", customObject, EnhancedType.of(CustomClass.class)) * .build(); *} *

Enhanced Document can be created with Json as input using Static factory method.In this case it used diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java index 4fbe5684b9bf..c5df2002ce77 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java @@ -21,7 +21,6 @@ import static org.hamcrest.Matchers.nullValue; import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; - import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; @@ -38,8 +37,6 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; -import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; @@ -48,23 +45,21 @@ import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocumentTestData; import software.amazon.awssdk.enhanced.dynamodb.document.TestData; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbAsyncTestBase; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; - @RunWith(Parameterized.class) public class BasicAsyncCrudTest extends LocalDynamoDbAsyncTestBase { private static final String ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS = "a*t:t.r-i#bute+3/4(&?5=@)<6>!ch$ar%"; private final TestData testData; + @Rule public ExpectedException exception = ExpectedException.none(); private DynamoDbEnhancedAsyncClient enhancedClient; @@ -73,7 +68,6 @@ public class BasicAsyncCrudTest extends LocalDynamoDbAsyncTestBase { private DynamoDbAsyncTable docMappedtable ; - @Before public void setUp(){ lowLevelClient = getDynamoDbAsyncClient(); @@ -89,9 +83,6 @@ public void setUp(){ .attributeConverterProviders(defaultProvider()) .build()); docMappedtable.createTable().join(); - - - } public BasicAsyncCrudTest(TestData testData) { @@ -125,7 +116,6 @@ private static Map appendKeysToTestDataAttributeMap(Map< return result; } - @After public void deleteTable() { getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() @@ -174,7 +164,6 @@ public void updateOverwriteCompleteItem_usingShortcutForm() { .putString("attribute2", "two") .putString("attribute3", "three") .build(); - docMappedtable.putItem(enhancedDocument).join(); // Updating new Items other than the one present in testData @@ -256,7 +245,6 @@ public void putThenDeleteItem_usingShortcutForm() { GetItemResponse lowLevelGetAfterDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); - assertThat(enhancedDocument.toMap(), is(EnhancedDocument.fromAttributeValueMap(lowLevelGetBeforeDelete.item()).toMap())); assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); assertThat(beforeDeleteResult.toMap(), is(lowLevelGetBeforeDelete.item())); @@ -314,8 +302,6 @@ public void putWithConditionThatSucceeds() { EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))).join(); assertThat(result.toMap(), is(newDoc.toMap())); - - } @Test @@ -419,8 +405,6 @@ public void updateOverwriteCompleteRecord_usingShortcutForm() { .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "six"); EnhancedDocument expectedDocument = updateDocBuilder.build(); - - // Explicitly Nullify each of the previous members testData.getEnhancedDocument().toMap().keySet().forEach(r -> { updateDocBuilder.putNull(r); @@ -479,8 +463,6 @@ public void updateOverwriteModelledNulls() { .putString("attribute2", "two") .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") .build(); - - docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); EnhancedDocument updateDocument = EnhancedDocument.builder().attributeConverterProviders(defaultProvider()) @@ -522,17 +504,13 @@ public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { .putNull("attribute") .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) .build(); - - EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) .item(updateDocument) .ignoreNulls(true) .build()).join(); - EnhancedDocument expectedResult = appendKeysToDoc(testData).toBuilder() .putString("attribute2", "two") .build(); - assertThat(result.toMap(), is(expectedResult.toMap())); } @@ -552,8 +530,6 @@ public void updateKeyOnlyExistingRecordDoesNothing() { @Test public void updateWithConditionThatSucceeds() { - - EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() .putString("attribute", "one") .putString("attribute2", "two") @@ -594,7 +570,6 @@ public void updateWithConditionThatFails() { .putString("attribute2", "two") .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") .build(); - docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); EnhancedDocument newDoc = EnhancedDocument.builder() diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java index 93c9b4fbb47d..e4068d2ce292 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -82,10 +82,7 @@ public void setUp(){ .attributeConverterProviders(defaultProvider()) .build()); docMappedtable.createTable(); - - - - } + } public BasicCrudTest(TestData testData) { this.testData = testData; @@ -118,7 +115,6 @@ private static Map appendKeysToTestDataAttributeMap(Map< return result; } - @After public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() @@ -161,13 +157,11 @@ public void getNonExistentItem() { @Test public void updateOverwriteCompleteItem_usingShortcutForm() { - EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() .putString("attribute", "one") .putString("attribute2", "two") .putString("attribute3", "three") .build(); - docMappedtable.putItem(enhancedDocument); // Updating new Items other than the one present in testData @@ -201,7 +195,6 @@ public void putTwiceThenGetItem() { .putString("attribute2", "two") .putString("attribute3", "three") .build(); - docMappedtable.putItem(enhancedDocument); // Updating new Items other than the one present in testData @@ -216,7 +209,6 @@ public void putTwiceThenGetItem() { docMappedtable.putItem(r -> r.item(updateDocument)); Map key = simpleKey(); GetItemResponse lowLevelGet = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); - // All the items are overwritten Assertions.assertThat(lowLevelGet.item()).isEqualTo(updateDocument.toMap()); @@ -233,9 +225,7 @@ public void putThenDeleteItem_usingShortcutForm() { .putString("attribute2", "two") .putString("attribute3", "three") .build(); - Map key = simpleKey(); - docMappedtable.putItem(r -> r.item(enhancedDocument)); GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); @@ -248,8 +238,6 @@ public void putThenDeleteItem_usingShortcutForm() { docMappedtable.getItem(Key.builder().partitionValue("id-value").sortValue("sort-value").build()); GetItemResponse lowLevelGetAfterDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)); - - assertThat(enhancedDocument.toMap(), is(EnhancedDocument.fromAttributeValueMap(lowLevelGetBeforeDelete.item()).toMap())); assertThat(beforeDeleteResult.toMap(), is(enhancedDocument.toMap())); assertThat(beforeDeleteResult.toMap(), is(lowLevelGetBeforeDelete.item())); @@ -265,7 +253,6 @@ public void putThenDeleteItem_usingKeyItemForm() { .putString("attribute2", "two") .putString("attribute3", "three") .build(); - docMappedtable.putItem(enhancedDocument); EnhancedDocument beforeDeleteResult = docMappedtable.deleteItem(enhancedDocument); @@ -282,13 +269,11 @@ public void putThenDeleteItem_usingKeyItemForm() { @Test public void putWithConditionThatSucceeds() { - EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() .putString("attribute", "one") .putString("attribute2", "two") .putString("attribute3", "three") .build(); - docMappedtable.putItem(r -> r.item(enhancedDocument)); @@ -307,8 +292,6 @@ public void putWithConditionThatSucceeds() { EnhancedDocument result = docMappedtable.getItem(r -> r.key(k -> k.partitionValue("id-value").sortValue("sort-value"))); assertThat(result.toMap(), is(newDoc.toMap())); - - } @Test @@ -319,7 +302,6 @@ public void putWithConditionThatFails() { .putString("attribute2", "two") .putString("attribute3", "three") .build(); - docMappedtable.putItem(r -> r.item(enhancedDocument)); @@ -363,7 +345,6 @@ public void deleteWithConditionThatSucceeds() { Key key = docMappedtable.keyFrom(enhancedDocument); docMappedtable.deleteItem(DeleteItemEnhancedRequest.builder().key(key).conditionExpression(conditionExpression).build()); - EnhancedDocument result = docMappedtable.getItem(r -> r.key(key)); assertThat(result, is(nullValue())); } @@ -398,7 +379,6 @@ public void updateOverwriteCompleteRecord_usingShortcutForm() { .putString("attribute2", "two") .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") .build(); - docMappedtable.putItem(enhancedDocument); // Updating new Items other than the one present in testData EnhancedDocument.Builder updateDocBuilder = EnhancedDocument.builder() @@ -410,8 +390,6 @@ public void updateOverwriteCompleteRecord_usingShortcutForm() { .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "six"); EnhancedDocument expectedDocument = updateDocBuilder.build(); - - // Explicitly Nullify each of the previous members testData.getEnhancedDocument().toMap().keySet().forEach(r -> { updateDocBuilder.putNull(r); @@ -471,8 +449,6 @@ public void updateOverwriteModelledNulls() { .putString("attribute2", "two") .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") .build(); - - docMappedtable.putItem(r -> r.item(enhancedDocument)); EnhancedDocument updateDocument = EnhancedDocument.builder().attributeConverterProviders(defaultProvider()) @@ -483,8 +459,6 @@ public void updateOverwriteModelledNulls() { .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS).build(); EnhancedDocument result = docMappedtable.updateItem(r -> r.item(updateDocument)); - - assertThat(result.isPresent("attribute2"), is(false)); assertThat(result.isPresent(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS), is(false)); assertThat(result.getString("attribute"), is("four")); @@ -505,7 +479,6 @@ public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { .putString("attribute2", "two") .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") .build(); - docMappedtable.putItem(r -> r.item(enhancedDocument)); EnhancedDocument updateDocument = EnhancedDocument.builder() @@ -514,13 +487,10 @@ public void updateCanIgnoreNullsDoesNotIgnoreNullAttributeValues() { .putNull("attribute") .putNull(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS) .build(); - - EnhancedDocument result = docMappedtable.updateItem(UpdateItemEnhancedRequest.builder(EnhancedDocument.class) .item(updateDocument) .ignoreNulls(true) .build()); - EnhancedDocument expectedResult = appendKeysToDoc(testData).toBuilder() .putString("attribute2", "two") .build(); @@ -579,7 +549,6 @@ public void updateWithConditionThatSucceeds() { @Test public void updateWithConditionThatFails() { - EnhancedDocument enhancedDocument = appendKeysToDoc(testData).toBuilder() .putString("attribute", "one") .putString("attribute2", "two") diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java index 45f4c11cd43b..8248566ddd08 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java @@ -24,8 +24,6 @@ import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween; @@ -34,10 +32,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; @@ -57,11 +53,9 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverter; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; @@ -70,11 +64,7 @@ import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; public class BasicQueryTest extends LocalDynamoDbSyncTestBase { - - - private DynamoDbClient lowLevelClient; - private DynamoDbTable docMappedtable ; private DynamoDbTable neseteddocMappedtable ; @@ -84,7 +74,6 @@ public class BasicQueryTest extends LocalDynamoDbSyncTestBase { private final String tableName = getConcreteTableName("doc-table-name"); private final String nestedTableName = getConcreteTableName("doc-nested-table-name"); - @Before public void createTable() { @@ -93,7 +82,6 @@ public void createTable() { enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(lowLevelClient) .build(); - docMappedtable = enhancedClient.table(tableName, TableSchema.documentSchemaBuilder() .attributeConverterProviders(defaultProvider()) @@ -105,7 +93,6 @@ public void createTable() { .attributeConverterProviders(defaultProvider()) .build()); docMappedtable.createTable(); - neseteddocMappedtable = enhancedClient.table(nestedTableName, TableSchema.documentSchemaBuilder() .attributeConverterProviders( @@ -132,8 +119,6 @@ public void deleteTable() { .build()); } - - private static final List DOCUMENTS = IntStream.range(0, 10) .mapToObj(i -> EnhancedDocument.builder() @@ -152,7 +137,6 @@ public void deleteTable() { .putNumber("sort", i) .putNumber("value", i) .build() - ).collect(Collectors.toList()); public static EnhancedDocument createDocumentFromNestedRecord(NestedTestRecord nestedTestRecord){ @@ -173,7 +157,6 @@ public static EnhancedDocument createDocumentFromNestedRecord(NestedTestRecord n enhancedDocument.put("innerAttributeRecord", innerAttributeRecord, EnhancedType.of(InnerAttributeRecord.class)); } return enhancedDocument.build(); - } private static final List NESTED_TEST_DOCUMENTS = @@ -208,9 +191,6 @@ public static EnhancedDocument createDocumentFromNestedRecord(NestedTestRecord n }) .collect(Collectors.toList()); - - - private void insertDocuments() { DOCUMENTS.forEach(document -> docMappedtable.putItem(r -> r.item(document))); NESTED_TEST_DOCUMENTS.forEach(nestedDocs -> neseteddocMappedtable.putItem(r -> r.item(nestedDocs))); @@ -237,13 +217,11 @@ public void queryAllRecordsDefaultSettings_shortcutForm() { .build() .toJson()).collect(Collectors.toList()))); assertThat(page.lastEvaluatedKey(), is(nullValue())); - } @Test public void queryAllRecordsDefaultSettings_withProjection() { insertDocuments(); - Iterator> results = docMappedtable.query(b -> b .queryConditional(keyEqualTo(k -> k.partitionValue("id-value"))) @@ -266,8 +244,6 @@ public void queryAllRecordsDefaultSettings_shortcutForm_viaItems() { PageIterable query = docMappedtable.query(keyEqualTo(k -> k.partitionValue("id-value"))); SdkIterable results = query.items(); - - assertThat(results.stream().map(i -> i.toJson()).collect(Collectors.toList()), is(DOCUMENTS.stream().map(i -> i .toBuilder() @@ -326,7 +302,6 @@ public void queryAllRecordsWithFilterAndProjection() { .attributesToProject("value") .build()) .iterator(); - assertThat(results.hasNext(), is(true)); Page page = results.next(); assertThat(results.hasNext(), is(false)); @@ -502,7 +477,6 @@ public void queryNestedRecord_withAttributeNameList() { assertThat(firstRecord.get("innerAttributeRecord", EnhancedType.of(InnerAttributeRecord.class)).getAttribTwo(), is(1)); } - @Test public void queryNestedRecord_withAttributeNameListAndStringAttributeToProjectAppended() { insertNestedDocuments(); @@ -529,7 +503,6 @@ public void queryNestedRecord_withAttributeNameListAndStringAttributeToProjectAp @Test public void queryAllRecordsDefaultSettings_withNestedProjectionNamesNotInNameMap() { insertNestedDocuments(); - Iterator> results = neseteddocMappedtable.query(b -> b .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-1"))) @@ -573,7 +546,6 @@ public void queryRecordDefaultSettings_withDotInTheName() { assertThat(record.getString("test.com"), is("v7")); } - @Test public void queryRecordDefaultSettings_withEmptyAttributeList() { insertNestedDocuments(); @@ -592,13 +564,10 @@ public void queryRecordDefaultSettings_withEmptyAttributeList() { assertThat(firstRecord.getString("test.com"), is("v7")); } - @Test public void queryRecordDefaultSettings_withNullAttributeList() { insertNestedDocuments(); - List backwardCompatibilty = null; - Iterator> results = neseteddocMappedtable.query(b -> b .queryConditional(keyEqualTo(k -> k.partitionValue("id-value-7"))) diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java index 97410a2f753d..6f0b9284f58c 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java @@ -24,8 +24,6 @@ import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; import java.util.ArrayList; import java.util.Arrays; @@ -34,7 +32,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; @@ -56,7 +53,6 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttributeRecord; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.NestedTestRecord; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.Page; import software.amazon.awssdk.enhanced.dynamodb.model.ScanEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @@ -85,14 +81,6 @@ public void createTable() { .dynamoDbClient(lowLevelClient) .build(); - mappedTable = enhancedClient.table(getConcreteTableName("table-name"), TABLE_SCHEMA); - - mappedNestedTable = enhancedClient.table(getConcreteTableName("nested-table-name"), - TableSchema.fromClass(NestedTestRecord.class)); - - mappedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - mappedNestedTable.createTable(r -> r.provisionedThroughput(getDefaultProvisionedThroughput())); - docMappedtable = enhancedClient.table(tableName, TableSchema.documentSchemaBuilder() .attributeConverterProviders(defaultProvider()) @@ -119,95 +107,6 @@ public void createTable() { neseteddocMappedtable.createTable(); } - - - private static class Record { - private String id; - private Integer sort; - - private String getId() { - return id; - } - - private Record setId(String id) { - this.id = id; - return this; - } - - private Integer getSort() { - return sort; - } - - private Record setSort(Integer sort) { - this.sort = sort; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Record record = (Record) o; - return Objects.equals(id, record.id) && - Objects.equals(sort, record.sort); - } - - @Override - public int hashCode() { - return Objects.hash(id, sort); - } - } - - private static final TableSchema TABLE_SCHEMA = - StaticTableSchema.builder(Record.class) - .newItemSupplier(Record::new) - .addAttribute(String.class, a -> a.name("id") - .getter(Record::getId) - .setter(Record::setId) - .tags(primaryPartitionKey())) - .addAttribute(Integer.class, a -> a.name("sort") - .getter(Record::getSort) - .setter(Record::setSort) - .tags(primarySortKey())) - .build(); - - private static final List RECORDS = - IntStream.range(0, 10) - .mapToObj(i -> new Record().setId("id-value").setSort(i)) - .collect(Collectors.toList()); - - private static final List NESTED_TEST_RECORDS = - IntStream.range(0, 10) - .mapToObj(i -> { - final NestedTestRecord nestedTestRecord = new NestedTestRecord(); - nestedTestRecord.setOuterAttribOne("id-value-" + i); - nestedTestRecord.setSort(i); - final InnerAttributeRecord innerAttributeRecord = new InnerAttributeRecord(); - innerAttributeRecord.setAttribOne("attribOne-"+i); - innerAttributeRecord.setAttribTwo(i); - nestedTestRecord.setInnerAttributeRecord(innerAttributeRecord); - nestedTestRecord.setDotVariable("v"+i); - return nestedTestRecord; - }) - .collect(Collectors.toList()); - - - private DynamoDbTable mappedTable; - - private DynamoDbTable mappedNestedTable; - - - private void insertRecords() { - RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record))); - } - - private void insertNestedRecords() { - NESTED_TEST_RECORDS.forEach(nestedTestRecord -> mappedNestedTable.putItem(r -> r.item(nestedTestRecord))); - } - - - - private static final List DOCUMENTS = IntStream.range(0, 10) .mapToObj(i -> EnhancedDocument.builder() @@ -268,8 +167,6 @@ public void deleteTable() { .build()); } - - @Test public void scanAllRecordsDefaultSettings() { insertDocuments(); @@ -288,7 +185,6 @@ public void scanAllRecordsDefaultSettings() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } - @Test public void queryAllRecordsDefaultSettings_withProjection() { insertDocuments(); @@ -300,15 +196,13 @@ public void queryAllRecordsDefaultSettings_withProjection() { Page page = results.next(); assertThat(results.hasNext(), is(false)); - assertThat(page.items().size(), is(RECORDS.size())); + assertThat(page.items().size(), is(DOCUMENTS.size())); EnhancedDocument firstRecord = page.items().get(0); assertThat(firstRecord.getString("id"), is(nullValue())); assertThat(firstRecord.getNumber("sort").intValue(), is(0)); } - - @Test public void scanAllRecordsDefaultSettings_viaItems() { insertDocuments(); @@ -317,14 +211,6 @@ public void scanAllRecordsDefaultSettings_viaItems() { is(DOCUMENTS_WITH_PROVIDERS.stream().map(i -> i.toJson()).collect(Collectors.toList()))); } - - - - - - - - @Test public void scanAllRecordsWithFilter() { insertDocuments(); @@ -405,7 +291,6 @@ public void scanLimit() { assertThat(page3.lastEvaluatedKey(), is(nullValue())); } - @Test public void scanLimit_viaItems() { insertDocuments(); @@ -445,7 +330,6 @@ public void scanExclusiveStartKey() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } - @Test public void scanExclusiveStartKey_viaItems() { insertDocuments(); @@ -461,6 +345,7 @@ private Map getKeyMap(int sort) { result.put("sort", numberValue(sort)); return Collections.unmodifiableMap(result); } + @Test public void scanAllRecordsWithFilterAndNestedProjectionSingleAttribute() { insertNestedDocuments(); @@ -593,7 +478,6 @@ public void scanAllRecordsWithNonExistigKeyName() { .putExpressionName("#sort", "sort") .build(); - Iterator> results = neseteddocMappedtable.scan( ScanEnhancedRequest.builder() diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java index c5f8bbf1506c..eceffa95a791 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java @@ -22,17 +22,12 @@ import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; @@ -49,8 +44,6 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; -import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.InnerAttribConverterProvider; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; import software.amazon.awssdk.enhanced.dynamodb.model.Page; @@ -63,8 +56,6 @@ public class IndexQueryTest extends LocalDynamoDbSyncTestBase { - - private DynamoDbClient lowLevelClient; private DynamoDbTable docMappedtable ; @@ -146,11 +137,8 @@ public void deleteTable() { getDynamoDbClient().deleteTable(DeleteTableRequest.builder() .tableName(tableName) .build()); - - } - @Test public void queryAllRecordsDefaultSettings_usingShortcutForm() { insertDocuments(); @@ -184,7 +172,6 @@ public void queryBetween() { assertThat(page.lastEvaluatedKey(), is(nullValue())); } - @Test public void queryLimit() { insertDocuments(); @@ -194,7 +181,6 @@ public void queryLimit() { .limit(5) .build()) .iterator(); - assertThat(results.hasNext(), is(true)); Page page1 = results.next(); assertThat(results.hasNext(), is(true)); @@ -223,7 +209,6 @@ public void queryLimit() { assertThat(page3.lastEvaluatedKey(), is(nullValue())); } - @Test public void queryEmpty() { Iterator> results = @@ -256,6 +241,4 @@ public void queryExclusiveStartKey() { is(KEYS_ONLY_DOCUMENTS.subList(8, 10).stream().map(i -> i.toMap()).collect(Collectors.toList()))); assertThat(page.lastEvaluatedKey(), is(nullValue())); } - - } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java index 7ac2559fb939..503444001597 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java @@ -22,17 +22,12 @@ import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; -import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.After; @@ -49,7 +44,6 @@ import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.LocalDynamoDbSyncTestBase; -import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; import software.amazon.awssdk.enhanced.dynamodb.model.Page; @@ -61,8 +55,6 @@ public class IndexScanTest extends LocalDynamoDbSyncTestBase { - - private DynamoDbClient lowLevelClient; private DynamoDbTable docMappedtable ; @@ -75,8 +67,6 @@ public class IndexScanTest extends LocalDynamoDbSyncTestBase { @Before public void createTable() { - - lowLevelClient = getDynamoDbClient(); enhancedClient = DynamoDbEnhancedClient.builder() .dynamoDbClient(lowLevelClient) @@ -192,9 +182,6 @@ public void scanLimit() { assertThat(results.hasNext(), is(true)); Page page3 = results.next(); assertThat(results.hasNext(), is(false)); - - - assertThat(page1.items().stream().map(i -> i.toMap()).collect(Collectors.toList()), is(KEYS_ONLY_DOCUMENTS.subList(0, 5).stream().map(i -> i.toMap()).collect(Collectors.toList()))); assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4))); From 6a4499d40292dd4adacc96c10fdb8c6c3b013fcb Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Mar 2023 11:38:02 -0700 Subject: [PATCH 3/7] Handled Review comments --- .../enhanced/dynamodb/document/EnhancedDocument.java | 8 ++++---- .../internal/document/DefaultEnhancedDocument.java | 8 +++++--- .../enhanced/dynamodb/mapper/DocumentTableSchema.java | 9 +++++---- .../enhanced/dynamodb/document/EnhancedDocumentTest.java | 9 +++++++++ .../dynamodb/document/ParameterizedDocumentTest.java | 9 +++++---- .../functionaltests/document/BasicAsyncCrudTest.java | 6 +++--- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java index fbd51ef47690..52e7e5e93e12 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -143,7 +143,7 @@ static Builder builder() { *

* Retrieving String Type for a document * {@snippet : - * Custom resultCustom = document.get("key", EnhancedType.of(String.class)); + * String resultCustom = document.get("key", EnhancedType.of(String.class)); * } * Retrieving Custom Type for which Convertor Provider was defined while creating the document * {@snippet : @@ -172,7 +172,7 @@ static Builder builder() { *

* Retrieving String Type for a document * {@snippet : - * Custom resultCustom = document.get("key", String.class); + * String resultCustom = document.get("key", String.class); * } * Retrieving Custom Type for which Convertor Provider was defined while creating the document * {@snippet : @@ -238,7 +238,7 @@ static Builder builder() { * Gets the Set of String values of the given attribute in the current document. * @param attributeName Name of the attribute. * @return value of the specified attribute in the current document as a set of SdkBytes; - * or null if the attribute either doesn't exist. + * or null if the attribute doesn't exist. */ Set getBytesSet(String attributeName); @@ -301,7 +301,7 @@ static Builder builder() { * @param attributeName Name of the attribute. * @return value of the specified attribute in the current document as a List of {@link AttributeValue} */ - List getListOfUnknownList(String attributeName); + List getListOfUnknownType(String attributeName); /** * Retrieves a Map with String keys and corresponding AttributeValue objects as values for a specified attribute in a diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index 5eebc8c81521..fc92cadfda42 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -199,7 +199,7 @@ public boolean getBoolean(String attributeName) { } @Override - public List getListOfUnknownList(String attributeName) { + public List getListOfUnknownType(String attributeName) { AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); if (attributeValue == null) { return null; @@ -403,6 +403,8 @@ public Builder putJson(String attributeName, String json) { @Override public Builder remove(String attributeName) { + Validate.isTrue(!(attributeName == null || attributeName.trim().length() == 0), + "Attribute name must not be null or empty"); nonAttributeValueMap.remove(attributeName); return this; } @@ -492,9 +494,9 @@ public int hashCode() { private static void checkAndValidateClass(Class type, boolean isPut) { Validate.paramNotNull(type, "type"); Validate.isTrue(!type.isAssignableFrom(List.class), - String.format(VALIDATE_TYPE_ERROR, List.class.getSimpleName(), isPut ? "put" : "get", "List")); + String.format(VALIDATE_TYPE_ERROR, "List", isPut ? "put" : "get", "List")); Validate.isTrue(!type.isAssignableFrom(Map.class), - String.format(VALIDATE_TYPE_ERROR, Map.class.getSimpleName(), isPut ? "put" : "get", "Map")); + String.format(VALIDATE_TYPE_ERROR, "Map", isPut ? "put" : "get", "Map")); } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java index 52048894e3f4..dfa7fee980d6 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java @@ -112,12 +112,13 @@ public Map itemToMap(EnhancedDocument item, boolean igno } private List mergeAttributeConverterProviders(EnhancedDocument item) { - Set providers = new LinkedHashSet<>(); - if (item.attributeConverterProviders() != null) { + if (item.attributeConverterProviders() != null && !item.attributeConverterProviders().isEmpty()) { + Set providers = new LinkedHashSet<>(); providers.addAll(item.attributeConverterProviders()); + providers.addAll(attributeConverterProviders); + return providers.stream().collect(Collectors.toList()); } - providers.addAll(attributeConverterProviders); - return providers.stream().collect(Collectors.toList()); + return attributeConverterProviders; } @Override diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java index bd90919f831e..3b38540e5fd5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -365,6 +365,15 @@ void removeParameterFromDocument() { assertThat(removedAttributesDoc.isNull("nullKey")).isFalse(); assertThat(removedAttributesDoc.isPresent("numberKey")).isFalse(); assertThat(removedAttributesDoc.getString("stringKey")).isEqualTo("stringValue"); + + assertThatIllegalArgumentException().isThrownBy( + () -> removedAttributesDoc.toBuilder().remove("")) + .withMessage("Attribute name must not be null or empty"); + + + assertThatIllegalArgumentException().isThrownBy( + () -> removedAttributesDoc.toBuilder().remove(null)) + .withMessage("Attribute name must not be null or empty"); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java index cfe5f0c7ca5c..96c69581c024 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/ParameterizedDocumentTest.java @@ -105,9 +105,10 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { break; case N: assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.getNumber(key).stringValue()); - assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.get(key, SdkNumber.class)); - assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.get(key, EnhancedType.of(SdkNumber.class))); - + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(String.valueOf(enhancedDocument.get(key, + SdkNumber.class))); + assertThat(enhancedAttributeValue.asNumber()).isEqualTo(enhancedDocument.get(key, + EnhancedType.of(SdkNumber.class)).toString()); break; case B: assertThat(enhancedAttributeValue.asBytes()).isEqualTo(enhancedDocument.getBytes(key)); @@ -139,7 +140,7 @@ void validateGetterMethodsOfDefaultDocument(TestData testData) { throw new IllegalStateException("Converter not found for " + enhancedType); } assertThat(converter.transformTo(value)).isEqualTo(enhancedDocument.getList(key, enhancedType)); - assertThat(enhancedDocument.getListOfUnknownList(key)).isEqualTo(value.l()); + assertThat(enhancedDocument.getListOfUnknownType(key)).isEqualTo(value.l()); break; case M: EnhancedType keyType = enhancedTypeMap.get(key).get(0); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java index c5df2002ce77..74d445228e5e 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java @@ -232,7 +232,7 @@ public void putThenDeleteItem_usingShortcutForm() { Map key = simpleKey(); - docMappedtable.putItem(r -> r.item(enhancedDocument)); + docMappedtable.putItem(r -> r.item(enhancedDocument)).join(); GetItemResponse lowLevelGetBeforeDelete = lowLevelClient.getItem(r -> r.key(key).tableName(tableName)).join(); @@ -261,7 +261,7 @@ public void putThenDeleteItem_usingKeyItemForm() { .putString("attribute3", "three") .build(); - docMappedtable.putItem(enhancedDocument); + docMappedtable.putItem(enhancedDocument).join(); EnhancedDocument beforeDeleteResult = docMappedtable.deleteItem(enhancedDocument).join(); EnhancedDocument afterDeleteResult = @@ -394,7 +394,7 @@ public void updateOverwriteCompleteRecord_usingShortcutForm() { .putString(ATTRIBUTE_NAME_WITH_SPECIAL_CHARACTERS, "three") .build(); - docMappedtable.putItem(enhancedDocument); + docMappedtable.putItem(enhancedDocument).join(); // Updating new Items other than the one present in testData EnhancedDocument.Builder updateDocBuilder = EnhancedDocument.builder() .attributeConverterProviders(defaultProvider()) From 84aca63fea5b43e4d075bbfa7dd77f1e49b79a69 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Mar 2023 13:18:37 -0700 Subject: [PATCH 4/7] Removed primitive boolean getter and replaced with Boolean getter --- .../dynamodb/document/EnhancedDocument.java | 13 ++++++++----- .../internal/document/DefaultEnhancedDocument.java | 5 ++--- .../dynamodb/document/EnhancedDocumentTest.java | 4 +--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java index 52e7e5e93e12..d406d59ab8af 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocument.java @@ -281,15 +281,18 @@ static Builder builder() { String getJson(String attributeName); /** - * Gets the boolean value for the specified attribute. + * Gets the {@link Boolean} value for the specified attribute. * * @param attributeName Name of the attribute. - * @return value of the specified attribute in the current document as a non-null Boolean. + * @return value of the specified attribute in the current document as a Boolean representation; or null if the attribute + * either doesn't exist or the attribute value is null. * @throws RuntimeException - * if either the attribute doesn't exist or if the attribute - * value cannot be converted into a boolean value. + * if the attribute value cannot be converted to a Boolean representation. + * Note that the Boolean representation of 0 and 1 in Numbers and "0" and "1" in Strings is false and true, + * respectively. + * */ - boolean getBoolean(String attributeName); + Boolean getBoolean(String attributeName); /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index fc92cadfda42..8c508379c96d 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -189,11 +189,10 @@ public String getJson(String attributeName) { } @Override - public boolean getBoolean(String attributeName) { + public Boolean getBoolean(String attributeName) { Boolean value = get(attributeName, Boolean.class); if (value == null) { - throw new IllegalStateException("Value of " + "attribute " + attributeName + " of type null cannot be converted" - + " into a boolean value"); + return null; } return value.booleanValue(); } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java index 3b38540e5fd5..0f2951711184 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -407,9 +407,7 @@ void accessingNulAttributeValue() { Assertions.assertNull(enhancedDocument.getString(NULL_KEY)); Assertions.assertNull(enhancedDocument.getList(NULL_KEY, EnhancedType.of(String.class))); - assertThatIllegalStateException().isThrownBy(() -> enhancedDocument.getBoolean(NULL_KEY)) - .withMessage("Value of attribute nullKey of type null cannot be converted into a " - + "boolean value"); + assertThat(enhancedDocument.getBoolean(NULL_KEY)).isNull(); } From 3808e75fefb202dadb36e8ac54c53158d80309dc Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Mar 2023 14:26:25 -0700 Subject: [PATCH 5/7] Spotbug issue fixed and using StringUtils --- .../internal/document/DefaultEnhancedDocument.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index 8c508379c96d..2750c19d18b7 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -20,6 +20,7 @@ import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.addEscapeCharacters; import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.stringValue; +import com.amazonaws.util.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -190,11 +191,7 @@ public String getJson(String attributeName) { @Override public Boolean getBoolean(String attributeName) { - Boolean value = get(attributeName, Boolean.class); - if (value == null) { - return null; - } - return value.booleanValue(); + return get(attributeName, Boolean.class); } @Override @@ -402,8 +399,7 @@ public Builder putJson(String attributeName, String json) { @Override public Builder remove(String attributeName) { - Validate.isTrue(!(attributeName == null || attributeName.trim().length() == 0), - "Attribute name must not be null or empty"); + Validate.isTrue(!StringUtils.isNullOrEmpty(attributeName), "Attribute name must not be null or empty"); nonAttributeValueMap.remove(attributeName); return this; } From f9fc6761bbd1427bcedd34a12d54559d94bb7d61 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Fri, 17 Mar 2023 15:46:40 -0700 Subject: [PATCH 6/7] StringUtils corrected the right package --- .../dynamodb/internal/document/DefaultEnhancedDocument.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index 2750c19d18b7..6fa958e6f09c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -20,7 +20,6 @@ import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.addEscapeCharacters; import static software.amazon.awssdk.enhanced.dynamodb.internal.document.JsonStringFormatHelper.stringValue; -import com.amazonaws.util.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -49,8 +48,10 @@ import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.utils.Lazy; +import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; + /** * Default implementation of {@link EnhancedDocument} used by the SDK to create Enhanced Documents. Attributes are initially saved * as a String-Object Map when documents are created using the builder. Conversion to an AttributeValueMap is done lazily when @@ -399,7 +400,7 @@ public Builder putJson(String attributeName, String json) { @Override public Builder remove(String attributeName) { - Validate.isTrue(!StringUtils.isNullOrEmpty(attributeName), "Attribute name must not be null or empty"); + Validate.isTrue(!StringUtils.isEmpty(attributeName), "Attribute name must not be null or empty"); nonAttributeValueMap.remove(attributeName); return this; } From 1d5ac8f9ea0047fe8d7b6a8fb6c973c60afa8649 Mon Sep 17 00:00:00 2001 From: John Viegas Date: Mon, 20 Mar 2023 23:18:23 +0530 Subject: [PATCH 7/7] Sonar quebe test bug fixed --- .../dynamodb/internal/document/DefaultEnhancedDocument.java | 2 +- .../enhanced/dynamodb/mapper/DocumentTableSchema.java | 2 -- .../enhanced/dynamodb/document/EnhancedDocumentTest.java | 6 +++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java index 6fa958e6f09c..ea0ec73c8ea4 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/document/DefaultEnhancedDocument.java @@ -263,7 +263,7 @@ private T fromAttributeValue(AttributeValue attributeValue, EnhancedType if (type.rawClass().equals(AttributeValue.class)) { return (T) attributeValue; } - return (T) converterForClass(type, attributeConverterChain).transformTo(attributeValue); + return converterForClass(type, attributeConverterChain).transformTo(attributeValue); } public static class DefaultBuilder implements EnhancedDocument.Builder { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java index dfa7fee980d6..9160e6977653 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/DocumentTableSchema.java @@ -68,8 +68,6 @@ */ @SdkPublicApi public final class DocumentTableSchema implements TableSchema { - - private static final AttributeValue NULL_ATTRIBUTE_VALUE = AttributeValue.fromNul(true); private final TableMetadata tableMetadata; private final List attributeConverterProviders; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java index 0f2951711184..a95f9b81c2d6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/document/EnhancedDocumentTest.java @@ -98,7 +98,7 @@ void enhancedDocumentGetters() { } @Test - public void enhancedDocWithNestedListAndMaps() { + void enhancedDocWithNestedListAndMaps() { /** * No attributeConverters supplied, in this case it uses the {@link DefaultAttributeConverterProvider} and does not error */ @@ -130,8 +130,8 @@ public void enhancedDocWithNestedListAndMaps() { assertThat(simpleDoc.getString("HashKey")).isEqualTo("abcdefg123"); assertThat(simpleDoc.isNull("nullKey")).isTrue(); - assertThat(simpleDoc.getNumber("numberKey")).isNotEqualTo(2.0); - assertThat(simpleDoc.getNumber("numberKey").doubleValue()).isEqualTo(2.0); + assertThat(simpleDoc.getNumber("numberKey")).isEqualTo(SdkNumber.fromDouble(2.0)); + assertThat(simpleDoc.getNumber("numberKey").bigDecimalValue().compareTo(BigDecimal.valueOf(2.0))).isEqualTo(0); assertThat(simpleDoc.getBytes("sdkByte")).isEqualTo(SdkBytes.fromUtf8String("a")); assertThat(simpleDoc.getBoolean("booleanKey")).isTrue();