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..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 @@ -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() + * .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 @@ -143,7 +143,7 @@ static Builder builder() { *

* Retrieving String Type for a document * {@snippet : - * Custom resultCustom = document.get("key", EnhancedType.of(Custom.class)); + * String 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 : + * String 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 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); @@ -259,10 +284,13 @@ static Builder builder() { * 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); @@ -276,7 +304,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 getListOfUnknownType(String attributeName); /** * Retrieves a Map with String keys and corresponding AttributeValue objects as values for a specified attribute in a @@ -287,7 +315,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 +378,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 +442,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 #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. + @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 #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 +478,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 +487,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 +504,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 +514,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..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 @@ -48,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 @@ -62,8 +64,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 +144,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,7 +187,7 @@ 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 @@ -190,7 +196,7 @@ public Boolean getBoolean(String attributeName) { } @Override - public List getUnknownTypeList(String attributeName) { + public List getListOfUnknownType(String attributeName) { AttributeValue attributeValue = attributeValueMap.getValue().get(attributeName); if (attributeValue == null) { return null; @@ -202,7 +208,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; @@ -257,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 { @@ -278,8 +284,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 +318,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 +333,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 +346,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 +355,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 +376,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 +398,13 @@ public Builder putJson(String attributeName, String json) { return putObject(attributeName, getAttributeValueFromJson(json)); } + @Override + public Builder remove(String attributeName) { + Validate.isTrue(!StringUtils.isEmpty(attributeName), "Attribute name must not be null or empty"); + nonAttributeValueMap.remove(attributeName); + return this; + } + @Override public Builder addAttributeConverterProvider(AttributeConverterProvider attributeConverterProvider) { Validate.paramNotNull(attributeConverterProvider, "attributeConverterProvider"); @@ -430,8 +460,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 +487,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", isPut ? "put" : "get", "List")); + Validate.isTrue(!type.isAssignableFrom(Map.class), + 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 13a4025fda91..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 @@ -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; @@ -66,7 +68,6 @@ */ @SdkPublicApi public final class DocumentTableSchema implements TableSchema { - private final TableMetadata tableMetadata; private final List attributeConverterProviders; @@ -109,12 +110,13 @@ public Map itemToMap(EnhancedDocument item, boolean igno } private List mergeAttributeConverterProviders(EnhancedDocument item) { - List providers = new ArrayList<>(); - 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; + 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 28a713550b7b..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 @@ -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 + 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")).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(); + 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() { - @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); - } + 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().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,137 @@ 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"); + + 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 + 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))); + assertThat(enhancedDocument.getBoolean(NULL_KEY)).isNull(); + + } + + @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..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 @@ -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,25 @@ 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(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)); + 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 +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.getUnknownTypeList(key)).isEqualTo(value.l()); + assertThat(enhancedDocument.getListOfUnknownType(key)).isEqualTo(value.l()); break; case M: EnhancedType keyType = enhancedTypeMap.get(key).get(0); @@ -142,7 +151,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..74d445228e5e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicAsyncCrudTest.java @@ -0,0 +1,596 @@ +/* + * 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.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.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.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)).join(); + 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).join(); + 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).join(); + // 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..e4068d2ce292 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicCrudTest.java @@ -0,0 +1,580 @@ +/* + * 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..8248566ddd08 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicQueryTest.java @@ -0,0 +1,609 @@ +/* + * 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.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.List; +import java.util.Map; +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.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.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..6f0b9284f58c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/BasicScanTest.java @@ -0,0 +1,671 @@ +/* + * 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 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.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.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(); + + 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 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(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(); + 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..eceffa95a791 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexQueryTest.java @@ -0,0 +1,244 @@ +/* + * 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.model.QueryConditional.keyEqualTo; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +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.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..503444001597 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/document/IndexScanTest.java @@ -0,0 +1,226 @@ +/* + * 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 java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +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.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