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