diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClientPreview-5c18892.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClientPreview-5c18892.json new file mode 100644 index 000000000000..44b9344c67cc --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClientPreview-5c18892.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "category": "Amazon DynamoDB Enhanced Client [Preview]", + "description": "Support for non-blocking asynchronous calling of all mapper operations" +} diff --git a/services-custom/dynamodb-enhanced/README.md b/services-custom/dynamodb-enhanced/README.md index 6b30443ad8a8..28403509e3d3 100644 --- a/services-custom/dynamodb-enhanced/README.md +++ b/services-custom/dynamodb-enhanced/README.md @@ -59,9 +59,9 @@ values used are also completely arbitrary. 3. Create a MappedDatabase object that you will use to repeatedly execute operations against all your tables :- ```java - MappedDatabase database = MappedDatabase.builder() - .dynamoDbClient(dynamoDbClient) - .build(); + MappedDatabase database = DynamoDbMappedDatabase.builder() + .dynamoDbClient(dynamoDbClient) + .build(); ``` 4. Create a MappedTable object that you will use to repeatedly execute operations against a specific table :- @@ -123,6 +123,42 @@ index. Here's an example of how to do this: Iterable> customersWithName = customersByName.query(equalTo(Key.of(stringValue("Smith")))); ``` +### Non-blocking asynchronous operations +If your application requires non-blocking asynchronous calls to +DynamoDb, then you can use the asynchronous implementation of the +mapper. It's very similar to the synchronous implementation with a few +key differences: + +1. When instantiating the mapped database, use the asynchronous version + of the library instead of the synchronous one (you will need to use + an asynchronous DynamoDb client from the SDK as well): + ```java + AsyncMappedDatabase database = DynamoDbAsyncMappedDatabase.builder() + .dynamoDbAsyncClient(dynamoDbAsyncClient) + .build(); + ``` + +2. Operations that return a single data item will return a + CompletableFuture of the result instead of just the result. Your + application can then do other work without having to block on the + result: + ```java + CompletableFuture result = mappedTable.execute(GetItem.of(customerKey)); + // Perform other work here + return result.join(); // now block and wait for the result + ``` + +3. Operations that return paginated lists of results will return an + SdkPublisher of the results instead of an SdkIterable. Your + application can then subscribe a handler to that publisher and deal + with the results asynchronously without having to block: + ```java + SdkPublisher results = mappedTable.execute(myQueryCommand); + results.subscribe(myCustomerResultsProcessor); + // Perform other work and let the processor handle the results asynchronously + ``` + + ### Using extensions The mapper supports plugin extensions to provide enhanced functionality beyond the simple primitive mapped operations. Only one extension can be diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedDatabase.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedDatabase.java new file mode 100644 index 000000000000..6354c968f7ec --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedDatabase.java @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import java.util.concurrent.CompletableFuture; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbAsyncMappedDatabase; + +/** + * Asynchronous interface for running commands against a DynamoDb database. See {@link DynamoDbAsyncMappedDatabase} for + * an implementation of this interface that can statically created. + */ +@SdkPublicApi +public interface AsyncMappedDatabase { + /** + * Executes a command against the database. + * + * @param operation The operation to be performed in the context of the database. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * + * @return A {@link CompletableFuture} of the result of the operation being executed. The documentation on the + * operation itself should have more information. + */ + CompletableFuture execute(DatabaseOperation operation); + + /** + * Returns a mapped table that can be used to execute commands that work with mapped items against that table. + * + * @param tableName The name of the physical table persisted by DynamoDb. + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @return A {@link AsyncMappedTable} object that can be used to execute table operations against. + * @param THe modelled object type being mapped to this table. + */ + AsyncMappedTable table(String tableName, TableSchema tableSchema); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedIndex.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedIndex.java new file mode 100644 index 000000000000..dadfa89fed90 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedIndex.java @@ -0,0 +1,84 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import java.util.concurrent.CompletableFuture; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; + +/** + * Asynchronous interface for running commands against an object that is linked to a specific DynamoDb secondary index + * and knows how to map records from the table that index is linked to into a modelled object. + * + * @param The type of the modelled object. + */ +@SdkPublicApi +public interface AsyncMappedIndex { + /** + * Executes a command that is expected to return a single data item against the database with the context of the + * specific table and secondary index this object is linked to. + * + * @param operationToPerform The operation to be performed in the context of the secondary index. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * @return A {@link CompletableFuture} of the result of the operation being executed. The documentation on the + * operation itself should have more information. + */ + CompletableFuture execute(IndexOperation operationToPerform); + + /** + * Executes a command that is expected to return a paginated list of data items against the database with the + * context of the specific table and secondary index this object is linked to. + * + * @param operationToPerform The operation to be performed in the context of the secondary index. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * @return An {@link SdkPublisher} that will publish successive pages of result data items to any subscriber with + * demand for them. + */ + SdkPublisher execute(PaginatedIndexOperation operationToPerform); + + /** + * Gets the {@link MapperExtension} associated with this mapped resource. + * @return The {@link MapperExtension} associated with this mapped resource. + */ + MapperExtension mapperExtension(); + + /** + * Gets the {@link TableSchema} object that this mapped table was built with. + * @return The {@link TableSchema} object for this mapped table. + */ + TableSchema tableSchema(); + + /** + * Gets the physical table name that operations performed by this object will be executed against. + * @return The physical table name. + */ + String tableName(); + + /** + * Gets the physical secondary index name that operations performed by this object will be executed against. + * @return The physical secondary index name. + */ + String indexName(); + + /** + * Creates a {@link Key} object from a modelled item. This key can be used in query conditionals and get + * operations to locate a specific record. + * @param item The item to extract the key fields from. + * @return A key that has been initialized with the index values extracted from the modelled object. + */ + Key keyFrom(T item); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedTable.java new file mode 100644 index 000000000000..5feaca050fa8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/AsyncMappedTable.java @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import java.util.concurrent.CompletableFuture; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; + +/** + * Asynchronous interface for running commands against an object that is linked to a specific DynamoDb table resource + * and therefore knows how to map records from that table into a modelled object. + * + * @param The type of the modelled object. + */ +@SdkPublicApi +public interface AsyncMappedTable extends MappedTableResource { + /** + * Returns a mapped index that can be used to execute commands against a secondary index belonging to the table + * being mapped by this object. Note that only a subset of the commands that work against a table will work + * against a secondary index. + * + * @param indexName The name of the secondary index to build the command interface for. + * @return An {@link AsyncMappedIndex} object that can be used to execute database commands against. + */ + AsyncMappedIndex index(String indexName); + + /** + * Executes a command that is expected to return a single data item against the database with the context of the + * primary index of the specific table this object is linked to. + ** + * @param operationToPerform The operation to be performed in the context of the primary index of the table. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * + * @return A {@link CompletableFuture} that will return the result of the operation being executed. The + * documentation on the operation itself should have more information. + */ + CompletableFuture execute(TableOperation operationToPerform); + + /** + * Executes a command that is expected to return a paginated list of data items against the database with the + * context of the primary index of the specific table this object is linked to. + ** + * @param operationToPerform The operation to be performed in the context of the primary index of the table. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * + * @return An {@link SdkPublisher} that will publish successive pages of result data items to any subscriber with + * demand for them. + */ + SdkPublisher execute(PaginatedTableOperation operationToPerform); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/CommonOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/CommonOperation.java index f4fd29bbac83..2d8dff7de6fd 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/CommonOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/CommonOperation.java @@ -15,25 +15,31 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; /** - * Common interface for a single operation that can be executed against a mapped database table. These operations can be - * made against either the primary index of a table or a secondary index, although some implementations of this - * interface do not support secondary indices and will throw an exception when executed against one. Conceptually an - * operation maps 1:1 with an actual DynamoDb call. - * + * Common interface for a single operation that can be executed in a synchronous or non-blocking asynchronous fashion + * against a mapped database table. These operations can be made against either the primary index of a table or a + * secondary index, although some implementations of this interface do not support secondary indices and will throw + * an exception when executed against one. Conceptually an operation maps 1:1 with an actual DynamoDb call. + *

* This interface is extended by {@link TableOperation} and {@link IndexOperation} which contain implementations of * the behavior to actually execute the operation in the context of a table or secondary index and are used by - * {@link MappedTable} and {@link MappedIndex} respectively. By sharing this common interface operations are able to - * re-use code regardless of whether they are executed in the context of a primary or secondary index. + * {@link MappedTable} or {@link AsyncMappedTable} and {@link MappedIndex} or {@link AsyncMappedIndex} respectively. By + * sharing this common interface operations are able to re-use code regardless of whether they are executed in the + * context of a primary or secondary index or whether they are being executed in a synchronous or non-blocking + * asynchronous fashion. * * @param The modelled object that this table maps records to. - * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient}. - * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient} or + * {@link DynamoDbAsyncClient}. + * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient} + * or {@link DynamoDbAsyncClient}. * @param The type of the mapped result object that will be returned by the execution of this operation. */ @SdkPublicApi @@ -49,12 +55,19 @@ public interface CommonOperation { RequestT generateRequest(TableSchema tableSchema, OperationContext context, MapperExtension mapperExtension); /** - * Provides a function for making the low level SDK call to DynamoDb. + * Provides a function for making the low level synchronous SDK call to DynamoDb. * @param dynamoDbClient A low level {@link DynamoDbClient} to make the call against. * @return A function that calls DynamoDb with a provided request object and returns the response object. */ Function serviceCall(DynamoDbClient dynamoDbClient); + /** + * Provides a function for making the low level non-blocking asynchronous SDK call to DynamoDb. + * @param dynamoDbAsyncClient A low level {@link DynamoDbAsyncClient} to make the call against. + * @return A function that calls DynamoDb with a provided request object and returns the response object. + */ + Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient); + /** * Takes the response object returned by the actual DynamoDb call and maps it into a higher level abstracted * result object. @@ -71,7 +84,8 @@ ResultT transformResponse(ResponseT response, MapperExtension mapperExtension); /** - * Default implementation of a complete execution of this operation against either the primary or a secondary index. + * Default implementation of a complete synchronous execution of this operation against either the primary or a + * secondary index. * It performs three steps: * 1) Call generateRequest() to get the request object. * 2) Call getServiceCall() and call it using the request object generated in the previous step. @@ -92,4 +106,30 @@ default ResultT execute(TableSchema tableSchema, ResponseT response = serviceCall(dynamoDbClient).apply(request); return transformResponse(response, tableSchema, context, mapperExtension); } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against either the + * primary or a secondary index. + * It performs three steps: + * 1) Call generateRequest() to get the request object. + * 2) Call getServiceCall() and call it using the request object generated in the previous step. + * 3) Wraps the {@link CompletableFuture} returned by the SDK in a new one that calls transformResponse() to + * convert the response object returned in the previous step to a high level result. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param context An object containing the context, or target, of the command execution. + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A {@link CompletableFuture} of the high level result object as specified by the implementation of this + * operation. + */ + default CompletableFuture executeAsync(TableSchema tableSchema, + OperationContext context, + MapperExtension mapperExtension, + DynamoDbAsyncClient dynamoDbAsyncClient) { + RequestT request = generateRequest(tableSchema, context, mapperExtension); + CompletableFuture response = asyncServiceCall(dynamoDbAsyncClient).apply(request); + return response.thenApply(r -> transformResponse(r, tableSchema, context, mapperExtension)); + } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/DatabaseOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/DatabaseOperation.java index 8960fbb8d1d3..64f3bcc86740 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/DatabaseOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/DatabaseOperation.java @@ -15,9 +15,11 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; /** @@ -25,10 +27,6 @@ * on a specific table or index, and may reference multiple tables and indexes (eg: batch operations). Conceptually an * operation maps 1:1 with an actual DynamoDb call. * - * Typically a database operation will be executed by a {@link MappedDatabase} which expects a supplier: - * - * {@code mappedDatabase.execute(() -> databaseOperation);} - ** * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient}. * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient}. * @param The type of the mapped result object that will be returned by the execution of this operation. @@ -44,12 +42,20 @@ public interface DatabaseOperation { RequestT generateRequest(MapperExtension mapperExtension); /** - * Provides a function for making the low level SDK call to DynamoDb. + * Provides a function for making the low level synchronous SDK call to DynamoDb. * @param dynamoDbClient A low level {@link DynamoDbClient} to make the call against. * @return A function that calls DynamoDb with a provided request object and returns the response object. */ Function serviceCall(DynamoDbClient dynamoDbClient); + /** + * Provides a function for making the low level non-blocking asynchronous SDK call to DynamoDb. + * @param dynamoDbAsyncClient A low level {@link DynamoDbAsyncClient} to make the call against. + * @return A function that calls DynamoDb with a provided request object and returns a {@link CompletableFuture} + * for the response object. + */ + Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient); + /** * Takes the response object returned by the actual DynamoDb call and maps it into a higher level abstracted * result object. @@ -61,7 +67,7 @@ public interface DatabaseOperation { ResultT transformResponse(ResponseT response, MapperExtension mapperExtension); /** - * Default implementation of a complete execution of this operation. It performs three steps: + * Default implementation of a complete synchronous execution of this operation. It performs three steps: * 1) Call generateRequest() to get the request object. * 2) Call getServiceCall() and call it using the request object generated in the previous step. * 3) Call transformResponse() to convert the response object returned in the previous step to a high level result. @@ -76,4 +82,25 @@ default ResultT execute(DynamoDbClient dynamoDbClient, MapperExtension mapperExt ResponseT response = serviceCall(dynamoDbClient).apply(request); return transformResponse(response, mapperExtension); } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation. It performs three + * steps: + * 1) Call generateRequest() to get the request object. + * 2) Call getServiceCall() and call it using the request object generated in the previous step. + * 3) Wraps the {@link CompletableFuture} returned by the SDK in a new one that calls transformResponse() to + * convert the response object returned in the previous step to a high level result. + * + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default CompletableFuture executeAsync(DynamoDbAsyncClient dynamoDbAsyncClient, + MapperExtension mapperExtension) { + + RequestT request = generateRequest(mapperExtension); + CompletableFuture response = asyncServiceCall(dynamoDbAsyncClient).apply(request); + return response.thenApply(r -> transformResponse(r, mapperExtension)); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperation.java index 8c059d467079..e510921c467d 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperation.java @@ -15,19 +15,16 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient; +import java.util.concurrent.CompletableFuture; + import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; /** * Interface for a single operation that can be executed against a secondary index of a mapped database table. * Conceptually an operation maps 1:1 with an actual DynamoDb call. - * - * Typically a table operation will be executed by a {@link MappedIndex}: - * - * {@code - * mappedDatabase.table(tableSchema).index("an_index_name").execute(indexOperation); // For secondary index - * } - * + *

* A concrete implementation of this interface should also implement {@link TableOperation} with the same types if * the operation supports being executed against both the primary index and secondary indices. * @@ -40,8 +37,8 @@ public interface IndexOperation extends CommonOperation { /** - * Default implementation of a complete execution of this operation against a secondary index. It will construct - * a context based on the given table name and secondary index name and then call execute() on the + * Default implementation of a complete synchronous execution of this operation against a secondary index. It will + * construct a context based on the given table name and secondary index name and then call execute() on the * {@link CommonOperation} interface to perform the operation. * * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. @@ -61,4 +58,27 @@ default ResultT executeOnSecondaryIndex(TableSchema tableSchema, OperationContext context = OperationContext.of(tableName, indexName); return execute(tableSchema, context, mapperExtension, dynamoDbClient); } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against a secondary + * index. It will construct a context based on the given table name and secondary index name and then call + * executeAsync() on the {@link CommonOperation} interface to perform the operation. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param tableName The physical name of the table that contains the secondary index to execute the operation + * against. + * @param indexName The physical name of the secondary index to execute the operation against. + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default CompletableFuture executeOnSecondaryIndexAsync(TableSchema tableSchema, + String tableName, + String indexName, + MapperExtension mapperExtension, + DynamoDbAsyncClient dynamoDbAsyncClient) { + OperationContext context = OperationContext.of(tableName, indexName); + return executeAsync(tableSchema, context, mapperExtension, dynamoDbAsyncClient); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/Key.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/Key.java index 572743b548ee..4c4a4eec9bf2 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/Key.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/Key.java @@ -27,7 +27,7 @@ * An object that represents a key that can be used to either identify a specific record or form part of a query * conditional. Keys are literal and hence not typed, and can be re-used in commands for different modelled types if * the literal values are to be the same. - * + *

* A key will always have a single partition key value associated with it, and optionally will have a sort key value. * The names of the keys themselves are not part of this object. */ diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedDatabase.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedDatabase.java index e4528648ba96..ab07a1856ea5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedDatabase.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedDatabase.java @@ -19,22 +19,14 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbMappedDatabase; /** - * Interface for running commands against a DynamoDb database. - * - * An implementation for this interface can be instantiated by using the default builder: - * - * MappedDatabase.builder() - * .dynamoDbClient(dynamoDbClient) - * .extendWith(mapperExtension) // Optional. See 'extensions' package. - * .build(); + * Synchronous interface for running commands against a DynamoDb database. See {@link DynamoDbMappedDatabase} for an + * implementation of this interface that can statically created. */ @SdkPublicApi public interface MappedDatabase { /** * Executes a command against the database. * - * Example: mappedDatabase.execute(BatchGetItem.of(...)); - * * @param operation The operation to be performed in the context of the database. * @param The expected return type from the operation. This is typically inferred by the compiler. * @return The result of the operation being executed. The documentation on the operation itself should have more diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedIndex.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedIndex.java index e7849b53f774..9c446366e56d 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedIndex.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedIndex.java @@ -16,36 +16,37 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbMappedTable; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; /** - * Interface for running commands against an object that is linked to a specific DynamoDb secondary index and knows - * how to map records from the table that index is linked to into a modelled object. This interface is extended by the - * {@link DynamoDbMappedTable} interface as all commands that can be made against a secondary index can also be made - * against the primary index. - * - * Typically an implementation for this interface can be obtained from a {@link MappedTable} which in turn can be - * obtained from a {@link MappedDatabase}: - * - * mappedIndex = mappedDatabase.table(tableSchema).index("gsi_1"); + * Synchronous interface for running commands against an object that is linked to a specific DynamoDb secondary index + * and knows how to map records from the table that index is linked to into a modelled object. * * @param The type of the modelled object. */ @SdkPublicApi public interface MappedIndex { /** - * Executes a command against the database with the context of the specific table and secondary index this object - * is linked to. - * - * Example: mappedIndex.execute(Scan.create()); + * Executes a command that is expected to return a single data item against the database with the context of the + * specific table and secondary index this object is linked to. * * @param operationToPerform The operation to be performed in the context of the secondary index. * @param The expected return type from the operation. This is typically inferred by the compiler. - * @return The result of the operation being executed. The documentation on the operation itself should have more - * information. + * @return the result of the operation being executed. The documentation on the operation itself should have + * more information. */ R execute(IndexOperation operationToPerform); + /** + * Executes a command that is expected to return a paginated list of data items against the database with the + * context of the specific table and secondary index this object is linked to. + * + * @param operationToPerform The operation to be performed in the context of the secondary index. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * @return An {@link SdkIterable} that will return successive pages of result data items as it is iterated over. + */ + SdkIterable execute(PaginatedIndexOperation operationToPerform); + /** * Gets the {@link MapperExtension} associated with this mapped resource. * @return The {@link MapperExtension} associated with this mapped resource. diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTable.java index f172af3e515c..7bb8a881914f 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTable.java @@ -16,19 +16,16 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; /** - * Interface for running commands against an object that is linked to a specific DynamoDb table and knows how to map - * records from that table into a modelled object. - * - * Typically an implementation for this interface can be obtained from a {@link MappedDatabase}: - * - * mappedTable = mappedDatabase.table(tableSchema); + * Synchronous interface for running commands against an object that is linked to a specific DynamoDb table resource + * and therefore knows how to map records from that table into a modelled object. * * @param The type of the modelled object. */ @SdkPublicApi -public interface MappedTable { +public interface MappedTable extends MappedTableResource { /** * Returns a mapped index that can be used to execute commands against a secondary index belonging to the table * being mapped by this object. Note that only a subset of the commands that work against a table will work @@ -40,41 +37,25 @@ public interface MappedTable { MappedIndex index(String indexName); /** - * Executes a command against the database with the context of the primary index of the specific table this object - * is linked to. - * - * Example: mappedTable.execute(PutItem.of(myItem)); - * + * Executes a command that is expected to return a single data item against the database with the context of the + * primary index of the specific table this object is linked to. + ** * @param operationToPerform The operation to be performed in the context of the primary index of the table. * @param The expected return type from the operation. This is typically inferred by the compiler. - * @return The result of the operation being executed. The documentation on the operation itself should have more - * information. + * + * @return the result of the operation being executed. The documentation on the operation itself should have + * more information. */ R execute(TableOperation operationToPerform); /** - * Gets the {@link MapperExtension} associated with this mapped resource. - * @return The {@link MapperExtension} associated with this mapped resource. - */ - MapperExtension mapperExtension(); - - /** - * Gets the {@link TableSchema} object that this mapped table was built with. - * @return The {@link TableSchema} object for this mapped table. - */ - TableSchema tableSchema(); - - /** - * Gets the physical table name that operations performed by this object will be executed against. - * @return The physical table name. - */ - String tableName(); - - /** - * Creates a {@link Key} object from a modelled item. This key can be used in query conditionals and get - * operations to locate a specific record. - * @param item The item to extract the key fields from. - * @return A key that has been initialized with the index values extracted from the modelled object. + * Executes a command that is expected to return a paginated list of data items against the database with the + * context of the primary index of the specific table this object is linked to. + ** + * @param operationToPerform The operation to be performed in the context of the primary index of the table. + * @param The expected return type from the operation. This is typically inferred by the compiler. + * + * @return An {@link SdkIterable} that will return successive pages of result data items as it is iterated over. */ - Key keyFrom(T item); + SdkIterable execute(PaginatedTableOperation operationToPerform); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTableResource.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTableResource.java new file mode 100644 index 000000000000..e40f499cedbd --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MappedTableResource.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Interface for a resource object that is part of either a {@link MappedTable} or {@link AsyncMappedTable}. This + * part of the interface is common between both of those higher order interfaces and has methods to access the + * metadata associated with the mapped entity, such as the schema and the table name, but knows nothing about how to + * actually execute operations against it. + * + * @param The type of the modelled object. + */ +@SdkPublicApi +public interface MappedTableResource { + /** + * Gets the {@link MapperExtension} associated with this mapped resource. + * @return The {@link MapperExtension} associated with this mapped resource. + */ + MapperExtension mapperExtension(); + + /** + * Gets the {@link TableSchema} object that this mapped table was built with. + * @return The {@link TableSchema} object for this mapped table. + */ + TableSchema tableSchema(); + + /** + * Gets the physical table name that operations performed by this object will be executed against. + * @return The physical table name. + */ + String tableName(); + + /** + * Creates a {@link Key} object from a modelled item. This key can be used in query conditionals and get + * operations to locate a specific record. + * @param item The item to extract the key fields from. + * @return A key that has been initialized with the index values extracted from the modelled object. + */ + Key keyFrom(T item); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MapperExtension.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MapperExtension.java index 62cc393e657f..81effe26384b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MapperExtension.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/MapperExtension.java @@ -27,7 +27,7 @@ * Interface for extending the mapper. Two hooks are provided, one that is called just before a record is written to * the database, and one called just after a record is read from the database. This gives the extension the * opportunity to act as an invisible layer between the application and the database and transform the data accordingly. - * + *

* Only one extension can be loaded with a mapped table. In order to combine multiple extensions, the * {@link ChainMapperExtension} should be used and initialized with all the component extensions to combine together * into a chain. diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedDatabaseOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedDatabaseOperation.java new file mode 100644 index 000000000000..7b4fa6d09e4f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedDatabaseOperation.java @@ -0,0 +1,114 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformIterable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformPublisher; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Interface for an operation that can be executed against a mapped database and is expected to return a paginated + * list of results. These operations do not operate on a specific table or index, and may reference multiple tables + * and indexes (eg: batch operations). Typically, each page of results that is served will automatically perform an + * additional service call to DynamoDb to retrieve the next set of results. + * + * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the mapped result object that will be returned by the execution of this operation. + */ +@SdkPublicApi +public interface PaginatedDatabaseOperation { + /** + * This method generates the request that needs to be sent to a low level {@link DynamoDbClient}. + * @param mapperExtension A {@link MapperExtension} that may modify the request of this operation. A null value + * here will result in no modifications. + * @return A request that can be used as an argument to a {@link DynamoDbClient} call to perform the operation. + */ + RequestT generateRequest(MapperExtension mapperExtension); + + /** + * Provides a function for making the low level synchronous paginated SDK call to DynamoDb. + * @param dynamoDbClient A low level {@link DynamoDbClient} to make the call against. + * @return A function that calls DynamoDb with a provided request object and returns the response object. + */ + Function> serviceCall(DynamoDbClient dynamoDbClient); + + /** + * Provides a function for making the low level non-blocking asynchronous paginated SDK call to DynamoDb. + * @param dynamoDbAsyncClient A low level {@link DynamoDbAsyncClient} to make the call against. + * @return A function that calls DynamoDb with a provided request object and returns the response object. + */ + Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient); + + /** + * Takes the response object returned by the actual DynamoDb call and maps it into a higher level abstracted + * result object. + * @param response The response object returned by the DynamoDb call for this operation. + * @param mapperExtension A {@link MapperExtension} that may modify the result of this operation. A null value + * here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + ResultT transformResponse(ResponseT response, MapperExtension mapperExtension); + + /** + * Default implementation of a complete synchronous execution of this operation against a database. + * It performs three steps: + * 1) Call generateRequest() to get the request object. + * 2) Call getServiceCall() and call it using the request object generated in the previous step. + * 3) Wraps the {@link SdkIterable} that was returned by the previous step with a transformation that turns each + * object returned to a high level result. + * + * @param dynamoDbClient A {@link DynamoDbClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return An {@link SdkIterable} that will iteratively return pages of high level result objects as specified by + * the implementation of this operation. + */ + default SdkIterable execute(DynamoDbClient dynamoDbClient, MapperExtension mapperExtension) { + RequestT request = generateRequest(mapperExtension); + SdkIterable response = serviceCall(dynamoDbClient).apply(request); + return TransformIterable.of(response, r -> transformResponse(r, mapperExtension)); + } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against a database. + * It performs three steps: + * 1) Call generateRequest() to get the request object. + * 2) Call getServiceCall() and call it using the request object generated in the previous step. + * 3) Wraps the {@link CompletableFuture} returned by the SDK in a new one that calls transformResponse() to + * convert the response object returned in the previous step to a high level result. + * + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return An {@link SdkPublisher} that will publish pages of the high level result object as specified by the + * implementation of this operation. + */ + default SdkPublisher executeAsync(DynamoDbAsyncClient dynamoDbAsyncClient, + MapperExtension mapperExtension) { + + RequestT request = generateRequest(mapperExtension); + SdkPublisher response = asyncServiceCall(dynamoDbAsyncClient).apply(request); + return TransformPublisher.of(response, r -> transformResponse(r, mapperExtension)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedIndexOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedIndexOperation.java new file mode 100644 index 000000000000..248792b19f22 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedIndexOperation.java @@ -0,0 +1,85 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Interface for an operation that can be executed against a secondary index of a mapped database table and is + * expected to return a paginated list of results. Typically, each page of results that is served will automatically + * perform an additional service call to DynamoDb to retrieve the next set of results. + *

+ * A concrete implementation of this interface should also implement {@link PaginatedTableOperation} with the same + * types if the operation supports being executed against both the primary index and secondary indices. + * + * @param The modelled object that this table maps records to. + * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the mapped result object that will be returned by the execution of this operation. + */ +@SdkPublicApi +public interface PaginatedIndexOperation + extends PaginatedOperation { + /** + * Default implementation of a complete synchronous execution of this operation against a secondary index. It will + * construct a context based on the given table name and secondary index name and then call execute() on the + * {@link PaginatedOperation} interface to perform the operation. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param tableName The physical name of the table that contains the secondary index to execute the operation + * against. + * @param indexName The physical name of the secondary index to execute the operation against. + * @param dynamoDbClient A {@link DynamoDbClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default SdkIterable executeOnSecondaryIndex(TableSchema tableSchema, + String tableName, + String indexName, + MapperExtension mapperExtension, + DynamoDbClient dynamoDbClient) { + OperationContext context = OperationContext.of(tableName, indexName); + return execute(tableSchema, context, mapperExtension, dynamoDbClient); + } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against a secondary + * index. It will construct a context based on the given table name and secondary index name and then call + * executeAsync() on the {@link PaginatedOperation} interface to perform the operation. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param tableName The physical name of the table that contains the secondary index to execute the operation + * against. + * @param indexName The physical name of the secondary index to execute the operation against. + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default SdkPublisher executeOnSecondaryIndexAsync(TableSchema tableSchema, + String tableName, + String indexName, + MapperExtension mapperExtension, + DynamoDbAsyncClient dynamoDbAsyncClient) { + OperationContext context = OperationContext.of(tableName, indexName); + return executeAsync(tableSchema, context, mapperExtension, dynamoDbAsyncClient); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedOperation.java new file mode 100644 index 000000000000..33b10c0a418f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedOperation.java @@ -0,0 +1,144 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import java.util.function.Function; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformIterable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformPublisher; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Common interface for an operation that can be executed in a synchronous or non-blocking asynchronous fashion + * against a mapped database table and is expected to return a paginated list of results. These operations can be made + * against either the primary index of a table or a secondary index, although some implementations of this interface + * do not support secondary indices and will throw an exception when executed against one. Typically, each page of + * results that is served will automatically perform an additional service call to DynamoDb to retrieve the next set + * of results. + *

+ * This interface is extended by {@link PaginatedTableOperation} and {@link PaginatedIndexOperation} which contain + * implementations of the behavior to actually execute the operation in the context of a table or secondary index and + * are used by {@link MappedTable} or {@link AsyncMappedTable} and {@link MappedIndex} or {@link AsyncMappedIndex} + * respectively. By sharing this common interface operations are able to re-use code regardless of whether they are + * executed in the context of a primary or secondary index or whether they are being executed in a synchronous or + * non-blocking asynchronous fashion. + * + * @param The modelled object that this table maps records to. + * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient} or + * {@link DynamoDbAsyncClient}. + * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient} + * or {@link DynamoDbAsyncClient}. + * @param The type of the mapped result object that will be returned by the execution of this operation. + */ +@SdkPublicApi +public interface PaginatedOperation { + /** + * This method generates the request that needs to be sent to a low level {@link DynamoDbClient}. + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param context An object containing the context, or target, of the command execution. + * @param mapperExtension A {@link MapperExtension} that may modify the request of this operation. A null value + * here will result in no modifications. + * @return A request that can be used as an argument to a {@link DynamoDbClient} call to perform the operation. + */ + RequestT generateRequest(TableSchema tableSchema, OperationContext context, MapperExtension mapperExtension); + + /** + * Provides a function for making the low level synchronous SDK call to DynamoDb. + * @param dynamoDbClient A low level {@link DynamoDbClient} to make the call against. + * @return A function that calls a paginated DynamoDb operation with a provided request object and returns the + * response object. + */ + Function> serviceCall(DynamoDbClient dynamoDbClient); + + /** + * Provides a function for making the low level non-blocking asynchronous SDK call to DynamoDb. + * @param dynamoDbAsyncClient A low level {@link DynamoDbAsyncClient} to make the call against. + * @return A function that calls a paginated DynamoDb operation with a provided request object and returns the + * response object. + */ + Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient); + + /** + * Takes the response object returned by the actual DynamoDb call and maps it into a higher level abstracted + * result object. + * @param response The response object returned by the DynamoDb call for this operation. + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param context An object containing the context, or target, of the command execution. + * @param mapperExtension A {@link MapperExtension} that may modify the result of this operation. A null value + * here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + ResultT transformResponse(ResponseT response, + TableSchema tableSchema, + OperationContext context, + MapperExtension mapperExtension); + + /** + * Default implementation of a complete synchronous execution of this operation against either the primary or a + * secondary index. + * It performs three steps: + * 1) Call generateRequest() to get the request object. + * 2) Call getServiceCall() and call it using the request object generated in the previous step. + * 3) Wraps the {@link SdkIterable} that was returned by the previous step with a transformation that turns each + * object returned to a high level result. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param context An object containing the context, or target, of the command execution. + * @param dynamoDbClient A {@link DynamoDbClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default SdkIterable execute(TableSchema tableSchema, + OperationContext context, + MapperExtension mapperExtension, + DynamoDbClient dynamoDbClient) { + RequestT request = generateRequest(tableSchema, context, mapperExtension); + SdkIterable response = serviceCall(dynamoDbClient).apply(request); + return TransformIterable.of(response, r -> transformResponse(r, tableSchema, context, mapperExtension)); + } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against either the + * primary or a secondary index. + * It performs three steps: + * 1) Call generateRequest() to get the request object. + * 2) Call getServiceCall() and call it using the request object generated in the previous step. + * 3) Wraps the {@link SdkPublisher} returned by the SDK in a new one that calls transformResponse() to + * convert the response objects published to a high level result. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param context An object containing the context, or target, of the command execution. + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return An {@link SdkPublisher} that will publish pages of the high level result object as specified by the + * implementation of this operation. + */ + default SdkPublisher executeAsync(TableSchema tableSchema, + OperationContext context, + MapperExtension mapperExtension, + DynamoDbAsyncClient dynamoDbAsyncClient) { + RequestT request = generateRequest(tableSchema, context, mapperExtension); + SdkPublisher response = asyncServiceCall(dynamoDbAsyncClient).apply(request); + + return TransformPublisher.of(response, r -> transformResponse(r, tableSchema, context, mapperExtension)); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedTableOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedTableOperation.java new file mode 100644 index 000000000000..508a91c06953 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/PaginatedTableOperation.java @@ -0,0 +1,82 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Interface for an operation that can be executed against a mapped database table and is expected to return a + * paginated list of results. These operations will be executed against the primary index of the table. Typically, + * each page of results that is served will automatically perform an additional service call to DynamoDb to retrieve + * the next set of results. + *

+ * A concrete implementation of this interface should also implement {@link PaginatedIndexOperation} with the same + * types if the operation supports being executed against both the primary index and secondary indices. + * + * @param The modelled object that this table maps records to. + * @param The type of the request object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the response object for the DynamoDb call in the low level {@link DynamoDbClient}. + * @param The type of the mapped result object that will be returned by the execution of this operation. + */ +@SdkPublicApi +public interface PaginatedTableOperation + extends PaginatedOperation { + /** + * Default implementation of a complete synchronous execution of this operation against the primary index. It will + * construct a context based on the given table name and then call execute() on the {@link PaginatedOperation} + * interface to perform the operation. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param tableName The physical name of the table to execute the operation against. + * @param dynamoDbClient A {@link DynamoDbClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default SdkIterable executeOnPrimaryIndex(TableSchema tableSchema, + String tableName, + MapperExtension mapperExtension, + DynamoDbClient dynamoDbClient) { + + OperationContext context = OperationContext.of(tableName, TableMetadata.primaryIndexName()); + return execute(tableSchema, context, mapperExtension, dynamoDbClient); + } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against the primary + * index. It will construct a context based on the given table name and then call executeAsync() on the + * {@link PaginatedOperation} interface to perform the operation. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param tableName The physical name of the table to execute the operation against. + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A high level result object as specified by the implementation of this operation. + */ + default SdkPublisher executeOnPrimaryIndexAsync(TableSchema tableSchema, + String tableName, + MapperExtension mapperExtension, + DynamoDbAsyncClient dynamoDbAsyncClient) { + + OperationContext context = OperationContext.of(tableName, TableMetadata.primaryIndexName()); + return executeAsync(tableSchema, context, mapperExtension, dynamoDbAsyncClient); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperation.java index 781a8fbaf134..44c72e3e35ae 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperation.java @@ -15,19 +15,16 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient; +import java.util.concurrent.CompletableFuture; + import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; /** * Interface for a single operation that can be executed against a mapped database table. These operations will be * executed against the primary index of the table. Conceptually an operation maps 1:1 with an actual DynamoDb call. - * - * Typically a table operation will be executed by a {@link MappedTable}: - * - * {@code - * mappedDatabase.table(tableSchema).execute(tableOperation); // For primary index - * } - * + *

* A concrete implementation of this interface should also implement {@link IndexOperation} with the same types if * the operation supports being executed against both the primary index and secondary indices. * @@ -40,8 +37,8 @@ public interface TableOperation extends CommonOperation { /** - * Default implementation of a complete execution of this operation against the primary index. It will construct - * a context based on the given table name and then call execute() on the {@link CommonOperation} interface to + * Default implementation of a complete synchronous execution of this operation against the primary index. It will + * construct a context based on the given table name and then call execute() on the {@link CommonOperation} interface to * perform the operation. * * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. @@ -58,4 +55,26 @@ default ResultT executeOnPrimaryIndex(TableSchema tableSchema, OperationContext context = OperationContext.of(tableName, TableMetadata.primaryIndexName()); return execute(tableSchema, context, mapperExtension, dynamoDbClient); } + + /** + * Default implementation of a complete non-blocking asynchronous execution of this operation against the primary + * index. It will construct a context based on the given table name and then call executeAsync() on the + * {@link CommonOperation} interface to perform the operation. + * + * @param tableSchema A {@link TableSchema} that maps the table to a modelled object. + * @param tableName The physical name of the table to execute the operation against. + * @param dynamoDbAsyncClient A {@link DynamoDbAsyncClient} to make the call against. + * @param mapperExtension A {@link MapperExtension} that may modify the request or result of this operation. A + * null value here will result in no modifications. + * @return A {@link CompletableFuture} of the high level result object as specified by the implementation of this + * operation. + */ + default CompletableFuture executeOnPrimaryIndexAsync(TableSchema tableSchema, + String tableName, + MapperExtension mapperExtension, + DynamoDbAsyncClient dynamoDbAsyncClient) { + + OperationContext context = OperationContext.of(tableName, TableMetadata.primaryIndexName()); + return executeAsync(tableSchema, context, mapperExtension, dynamoDbAsyncClient); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableSchema.java index 7af090b8c149..d291bdca77b2 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableSchema.java @@ -34,7 +34,7 @@ public interface TableSchema { /** * Takes a raw DynamoDb SDK representation of a record in a table and maps it to a Java object. A new object is * created to fulfil this operation. - * + *

* If attributes are missing from the map, that will not cause an error, however if attributes are found in the * map which the mapper does not know how to map, an exception will be thrown. * diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedDatabase.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedDatabase.java new file mode 100644 index 000000000000..e023b55ddeaf --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedDatabase.java @@ -0,0 +1,123 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import java.util.concurrent.CompletableFuture; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.DatabaseOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +@SdkPublicApi +@ThreadSafe +public final class DynamoDbAsyncMappedDatabase implements AsyncMappedDatabase { + private final DynamoDbAsyncClient dynamoDbClient; + private final MapperExtension mapperExtension; + + private DynamoDbAsyncMappedDatabase(DynamoDbAsyncClient dynamoDbClient, MapperExtension mapperExtension) { + this.dynamoDbClient = dynamoDbClient; + this.mapperExtension = mapperExtension; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public CompletableFuture execute(DatabaseOperation operation) { + return operation.executeAsync(dynamoDbClient, mapperExtension); + } + + @Override + public DynamoDbAsyncMappedTable table(String tableName, TableSchema tableSchema) { + return new DynamoDbAsyncMappedTable<>(dynamoDbClient, mapperExtension, tableSchema, tableName); + } + + public DynamoDbAsyncClient dynamoDbAsyncClient() { + return dynamoDbClient; + } + + public MapperExtension mapperExtension() { + return mapperExtension; + } + + public Builder toBuilder() { + return builder().dynamoDbClient(this.dynamoDbClient).extendWith(this.mapperExtension); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DynamoDbAsyncMappedDatabase that = (DynamoDbAsyncMappedDatabase) o; + + if (dynamoDbClient != null ? ! dynamoDbClient.equals(that.dynamoDbClient) + : that.dynamoDbClient != null) { + + return false; + } + return mapperExtension != null ? mapperExtension.equals(that.mapperExtension) : that.mapperExtension == null; + } + + @Override + public int hashCode() { + int result = dynamoDbClient != null ? dynamoDbClient.hashCode() : 0; + result = 31 * result + (mapperExtension != null ? mapperExtension.hashCode() : 0); + return result; + } + + public static final class Builder { + private DynamoDbAsyncClient dynamoDbClient; + private MapperExtension mapperExtension; + + private Builder() { + } + + public DynamoDbAsyncMappedDatabase build() { + if (dynamoDbClient == null) { + throw new IllegalArgumentException("You must provide a DynamoDbClient to build a " + + "DynamoDbMappedDatabase."); + } + + return new DynamoDbAsyncMappedDatabase(dynamoDbClient, mapperExtension); + } + + public Builder dynamoDbClient(DynamoDbAsyncClient dynamoDbAsyncClient) { + this.dynamoDbClient = dynamoDbAsyncClient; + return this; + } + + public Builder extendWith(MapperExtension mapperExtension) { + if (mapperExtension != null && this.mapperExtension != null) { + throw new IllegalArgumentException("You may only extend a DynamoDbMappedDatabase with a single " + + "extension. To combine multiple extensions, use the " + + "ChainMapperExtension."); + } + + this.mapperExtension = mapperExtension; + return this; + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedIndex.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedIndex.java new file mode 100644 index 000000000000..1245ed91e192 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedIndex.java @@ -0,0 +1,136 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.core.Utils.createKeyFromItem; + +import java.util.concurrent.CompletableFuture; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedIndex; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.IndexOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedIndexOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +@SdkPublicApi +@ThreadSafe +public final class DynamoDbAsyncMappedIndex implements AsyncMappedIndex { + private final DynamoDbAsyncClient dynamoDbClient; + private final MapperExtension mapperExtension; + private final TableSchema tableSchema; + private final String tableName; + private final String indexName; + + DynamoDbAsyncMappedIndex(DynamoDbAsyncClient dynamoDbClient, + MapperExtension mapperExtension, + TableSchema tableSchema, + String tableName, + String indexName) { + this.dynamoDbClient = dynamoDbClient; + this.mapperExtension = mapperExtension; + this.tableSchema = tableSchema; + this.tableName = tableName; + this.indexName = indexName; + } + + @Override + public CompletableFuture execute(IndexOperation operationToPerform) { + return operationToPerform.executeOnSecondaryIndexAsync(tableSchema, + tableName, + indexName, + mapperExtension, + dynamoDbClient); + } + + @Override + public SdkPublisher execute(PaginatedIndexOperation operationToPerform) { + return operationToPerform.executeOnSecondaryIndexAsync(tableSchema, + tableName, + indexName, + mapperExtension, + dynamoDbClient); + } + + @Override + public MapperExtension mapperExtension() { + return this.mapperExtension; + } + + @Override + public TableSchema tableSchema() { + return tableSchema; + } + + public DynamoDbAsyncClient dynamoDbClient() { + return dynamoDbClient; + } + + public String tableName() { + return tableName; + } + + public String indexName() { + return indexName; + } + + @Override + public Key keyFrom(T item) { + return createKeyFromItem(item, tableSchema, indexName); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DynamoDbAsyncMappedIndex that = (DynamoDbAsyncMappedIndex) o; + + if (dynamoDbClient != null ? ! dynamoDbClient.equals(that.dynamoDbClient) + : that.dynamoDbClient != null) { + + return false; + } + if (mapperExtension != null ? ! mapperExtension.equals(that.mapperExtension) : that.mapperExtension != null) { + return false; + } + if (tableSchema != null ? ! tableSchema.equals(that.tableSchema) : that.tableSchema != null) { + return false; + } + if (tableName != null ? ! tableName.equals(that.tableName) : that.tableName != null) { + return false; + } + return indexName != null ? indexName.equals(that.indexName) : that.indexName == null; + } + + @Override + public int hashCode() { + int result = dynamoDbClient != null ? dynamoDbClient.hashCode() : 0; + result = 31 * result + (mapperExtension != null ? mapperExtension.hashCode() : 0); + result = 31 * result + (tableSchema != null ? tableSchema.hashCode() : 0); + result = 31 * result + (tableName != null ? tableName.hashCode() : 0); + result = 31 * result + (indexName != null ? indexName.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedTable.java new file mode 100644 index 000000000000..fd21e25b50de --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedTable.java @@ -0,0 +1,132 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.core.Utils.createKeyFromItem; + +import java.util.concurrent.CompletableFuture; + +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedTableOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +@SdkPublicApi +@ThreadSafe +public final class DynamoDbAsyncMappedTable implements AsyncMappedTable { + private final DynamoDbAsyncClient dynamoDbClient; + private final MapperExtension mapperExtension; + private final TableSchema tableSchema; + private final String tableName; + + DynamoDbAsyncMappedTable(DynamoDbAsyncClient dynamoDbClient, + MapperExtension mapperExtension, + TableSchema tableSchema, + String tableName) { + this.dynamoDbClient = dynamoDbClient; + this.mapperExtension = mapperExtension; + this.tableSchema = tableSchema; + this.tableName = tableName; + } + + @Override + public CompletableFuture execute(TableOperation operationToPerform) { + return operationToPerform.executeOnPrimaryIndexAsync(tableSchema, + tableName, + mapperExtension, + dynamoDbClient); + } + + @Override + public SdkPublisher execute(PaginatedTableOperation operationToPerform) { + return operationToPerform.executeOnPrimaryIndexAsync(tableSchema, + tableName, + mapperExtension, + dynamoDbClient); + } + + @Override + public MapperExtension mapperExtension() { + return this.mapperExtension; + } + + @Override + public TableSchema tableSchema() { + return this.tableSchema; + } + + public DynamoDbAsyncClient dynamoDbClient() { + return dynamoDbClient; + } + + public String tableName() { + return tableName; + } + + @Override + public DynamoDbAsyncMappedIndex index(String indexName) { + // Force a check for the existence of the index + tableSchema.tableMetadata().indexPartitionKey(indexName); + + return new DynamoDbAsyncMappedIndex<>(dynamoDbClient, mapperExtension, tableSchema, tableName, indexName); + } + + @Override + public Key keyFrom(T item) { + return createKeyFromItem(item, tableSchema, TableMetadata.primaryIndexName()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DynamoDbAsyncMappedTable that = (DynamoDbAsyncMappedTable) o; + + if (dynamoDbClient != null ? ! dynamoDbClient.equals(that.dynamoDbClient) + : that.dynamoDbClient != null) { + + return false; + } + if (mapperExtension != null ? ! mapperExtension.equals(that.mapperExtension) : that.mapperExtension != null) { + return false; + } + if (tableSchema != null ? ! tableSchema.equals(that.tableSchema) : that.tableSchema != null) { + return false; + } + return tableName != null ? tableName.equals(that.tableName) : that.tableName == null; + } + + @Override + public int hashCode() { + int result = dynamoDbClient != null ? dynamoDbClient.hashCode() : 0; + result = 31 * result + (mapperExtension != null ? mapperExtension.hashCode() : 0); + result = 31 * result + (tableSchema != null ? tableSchema.hashCode() : 0); + result = 31 * result + (tableName != null ? tableName.hashCode() : 0); + return result; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedIndex.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedIndex.java index c3a0fbdefa3f..9f74bd265e27 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedIndex.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedIndex.java @@ -19,10 +19,12 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.IndexOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedIndex; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedIndexOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @@ -56,6 +58,15 @@ public R execute(IndexOperation operationToPerform) { dynamoDbClient); } + @Override + public SdkIterable execute(PaginatedIndexOperation operationToPerform) { + return operationToPerform.executeOnSecondaryIndex(tableSchema, + tableName, + indexName, + mapperExtension, + dynamoDbClient); + } + @Override public MapperExtension mapperExtension() { return this.mapperExtension; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedTable.java index 05a975224d94..acfd95d79212 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbMappedTable.java @@ -19,9 +19,11 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedTableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; @@ -50,6 +52,11 @@ public R execute(TableOperation operationToPerform) { return operationToPerform.executeOnPrimaryIndex(tableSchema, tableName, mapperExtension, dynamoDbClient); } + @Override + public SdkIterable execute(PaginatedTableOperation operationToPerform) { + return operationToPerform.executeOnPrimaryIndex(tableSchema, tableName, mapperExtension, dynamoDbClient); + } + @Override public MapperExtension mapperExtension() { return this.mapperExtension; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterable.java index 687ea2211c86..40c45444f1b4 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterable.java @@ -19,9 +19,11 @@ import java.util.function.Function; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; +// TODO: Consider moving to SDK core @SdkInternalApi -public class TransformIterable implements Iterable { +public class TransformIterable implements SdkIterable { private final Iterable wrappedIterable; private final Function transformFunction; @@ -30,7 +32,7 @@ private TransformIterable(Iterable wrappedIterable, Function transformF this.transformFunction = transformFunction; } - public static TransformIterable of(Iterable iterable, Function transformFunction) { + public static TransformIterable of(SdkIterable iterable, Function transformFunction) { return new TransformIterable<>(iterable, transformFunction); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterator.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterator.java index 14237243e111..b25e8bc44706 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterator.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformIterator.java @@ -20,6 +20,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; +// TODO: Consider moving to SDK core @SdkInternalApi public class TransformIterator implements Iterator { private final Iterator wrappedIterator; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformPublisher.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformPublisher.java new file mode 100644 index 000000000000..71806e46242c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/TransformPublisher.java @@ -0,0 +1,74 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import java.util.function.Function; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.async.SdkPublisher; + +// TODO: Consider moving to SDK core +@SdkInternalApi +public class TransformPublisher implements SdkPublisher { + private final SdkPublisher wrappedPublisher; + private final Function transformFunction; + + private TransformPublisher(SdkPublisher wrappedPublisher, Function transformFunction) { + this.wrappedPublisher = wrappedPublisher; + this.transformFunction = transformFunction; + } + + public static TransformPublisher of(SdkPublisher publisher, Function transformFunction) { + return new TransformPublisher<>(publisher, transformFunction); + } + + @Override + public void subscribe(Subscriber subscriber) { + wrappedPublisher.subscribe(new TransformSubscriber(subscriber)); + } + + private final class TransformSubscriber implements Subscriber { + private final Subscriber delegateSubscriber; + + private TransformSubscriber(Subscriber delegateSubscriber) { + this.delegateSubscriber = delegateSubscriber; + } + + + @Override + public void onSubscribe(Subscription subscription) { + delegateSubscriber.onSubscribe(subscription); + } + + @Override + public void onNext(T t) { + delegateSubscriber.onNext(transformFunction.apply(t)); + } + + @Override + public void onError(Throwable throwable) { + delegateSubscriber.onError(throwable); + } + + @Override + public void onComplete() { + delegateSubscriber.onComplete(); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/Utils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/Utils.java index ad2736de8670..29611c6b3e12 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/Utils.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/Utils.java @@ -78,35 +78,34 @@ public static T readAndTransformSingleItem(Map itemM return tableSchema.mapToItem(itemMap); } - public static Function> readAndTransformPaginatedItems( + public static Page readAndTransformPaginatedItems( + ResponseT response, TableSchema tableSchema, OperationContext operationContext, MapperExtension mapperExtension, Function>> getItems, Function> getLastEvaluatedKey) { - return response -> { - if (getLastEvaluatedKey.apply(response) == null || getLastEvaluatedKey.apply(response).isEmpty()) { - // Last page - return Page.of(getItems.apply(response) - .stream() - .map(itemMap -> readAndTransformSingleItem(itemMap, - tableSchema, - operationContext, - mapperExtension)) - .collect(Collectors.toList())); - } else { - // More pages to come; add the lastEvaluatedKey - return Page.of(getItems.apply(response) - .stream() - .map(itemMap -> readAndTransformSingleItem(itemMap, - tableSchema, - operationContext, - mapperExtension)) - .collect(Collectors.toList()), - getLastEvaluatedKey.apply(response)); - } - }; + if (getLastEvaluatedKey.apply(response) == null || getLastEvaluatedKey.apply(response).isEmpty()) { + // Last page + return Page.of(getItems.apply(response) + .stream() + .map(itemMap -> readAndTransformSingleItem(itemMap, + tableSchema, + operationContext, + mapperExtension)) + .collect(Collectors.toList())); + } else { + // More pages to come; add the lastEvaluatedKey + return Page.of(getItems.apply(response) + .stream() + .map(itemMap -> readAndTransformSingleItem(itemMap, + tableSchema, + operationContext, + mapperExtension)) + .collect(Collectors.toList()), + getLastEvaluatedKey.apply(response)); + } } public static Key createKeyFromItem(T item, TableSchema tableSchema, String indexName) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItem.java index fef6b0cc2080..5d64dfea16a7 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItem.java @@ -28,23 +28,23 @@ import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.DatabaseOperation; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformIterable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedDatabaseOperation; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest; import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; -import software.amazon.awssdk.services.dynamodb.paginators.BatchGetItemIterable; @SdkPublicApi public class BatchGetItem - implements DatabaseOperation> { + implements PaginatedDatabaseOperation { + private final Collection readBatches; private BatchGetItem(Collection readBatches) { @@ -78,17 +78,22 @@ public BatchGetItemRequest generateRequest(MapperExtension mapperExtension) { } @Override - public Iterable transformResponse(BatchGetItemIterable response, - MapperExtension mapperExtension) { - return TransformIterable.of(response, - batchGetItemResponse -> new ResultsPage(batchGetItemResponse, mapperExtension)); + public ResultsPage transformResponse(BatchGetItemResponse response, MapperExtension mapperExtension) { + return new ResultsPage(response, mapperExtension); } @Override - public Function serviceCall(DynamoDbClient dynamoDbClient) { + public Function> serviceCall(DynamoDbClient dynamoDbClient) { return dynamoDbClient::batchGetItemPaginator; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::batchGetItemPaginator; + } + public Collection readBatches() { return readBatches; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchWriteItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchWriteItem.java index ffb080358518..f92ff3d367b5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchWriteItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchWriteItem.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -31,6 +32,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemRequest; import software.amazon.awssdk.services.dynamodb.model.BatchWriteItemResponse; @@ -85,6 +87,13 @@ public Function serviceCall(Dynam return dynamoDbClient::batchWriteItem; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::batchWriteItem; + } + public Collection writeBatches() { return writeBatches; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/CreateTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/CreateTable.java index 5f086b780f61..da9dd1598189 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/CreateTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/CreateTable.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -31,6 +32,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; import software.amazon.awssdk.services.dynamodb.model.BillingMode; @@ -159,6 +161,13 @@ public Function serviceCall(DynamoDbCli return dynamoDbClient::createTable; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::createTable; + } + @Override public Void transformResponse(CreateTableResponse response, TableSchema tableSchema, diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/DeleteItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/DeleteItem.java index 6c65b196555f..a01c956ac75a 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/DeleteItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/DeleteItem.java @@ -17,6 +17,7 @@ import static software.amazon.awssdk.extensions.dynamodb.mappingclient.core.Utils.readAndTransformSingleItem; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -29,6 +30,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TransactableWriteOperation; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.Delete; import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; @@ -108,6 +110,13 @@ public Function serviceCall(DynamoDbClien return dynamoDbClient::deleteItem; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::deleteItem; + } + @Override public WriteRequest generateWriteRequest(TableSchema tableSchema, OperationContext operationContext, diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/GetItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/GetItem.java index 0fd3a59801f2..99a8039ecf51 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/GetItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/GetItem.java @@ -17,6 +17,7 @@ import static software.amazon.awssdk.extensions.dynamodb.mappingclient.core.Utils.readAndTransformSingleItem; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -28,6 +29,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TransactableReadOperation; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.Get; import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; @@ -97,6 +99,13 @@ public Function serviceCall(DynamoDbClient dyna return dynamoDbClient::getItem; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::getItem; + } + @Override public TransactGetItem generateTransactGetItem(TableSchema tableSchema, OperationContext operationContext, diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/PutItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/PutItem.java index 1063ece314f8..d61315d130f3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/PutItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/PutItem.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient.operations; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -28,6 +29,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TransactableWriteOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.extensions.WriteModification; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.Put; @@ -133,6 +135,13 @@ public Function serviceCall(DynamoDbClient dyna return dynamoDbClient::putItem; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::putItem; + } + @Override public WriteRequest generateWriteRequest(TableSchema tableSchema, OperationContext operationContext, diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Query.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Query.java index 1024f24d0efc..bea453374c9b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Query.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Query.java @@ -21,24 +21,25 @@ import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.IndexOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Page; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedIndexOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedTableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformIterable; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.QueryRequest; import software.amazon.awssdk.services.dynamodb.model.QueryResponse; -import software.amazon.awssdk.services.dynamodb.paginators.QueryIterable; @SdkPublicApi -public class Query implements TableOperation>>, - IndexOperation>> { +public class Query implements PaginatedTableOperation>, + PaginatedIndexOperation> { private final QueryConditional queryConditional; private final Map exclusiveStartKey; @@ -114,22 +115,26 @@ public QueryRequest generateRequest(TableSchema tableSchema, } @Override - public Iterable> transformResponse(QueryIterable response, - TableSchema tableSchema, - OperationContext operationContext, - MapperExtension mapperExtension) { - return TransformIterable.of(response, getQueryResponseMapper(tableSchema, operationContext, mapperExtension)); + public Function> serviceCall(DynamoDbClient dynamoDbClient) { + return dynamoDbClient::queryPaginator; } @Override - public Function serviceCall(DynamoDbClient dynamoDbClient) { - return dynamoDbClient::queryPaginator; + public Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient) { + return dynamoDbAsyncClient::queryPaginator; } - private Function> getQueryResponseMapper(TableSchema tableSchema, - OperationContext operationContext, - MapperExtension mapperExtension) { - return readAndTransformPaginatedItems(tableSchema, operationContext, mapperExtension, QueryResponse::items, + @Override + public Page transformResponse(QueryResponse response, + TableSchema tableSchema, + OperationContext context, + MapperExtension mapperExtension) { + + return readAndTransformPaginatedItems(response, + tableSchema, + context, + mapperExtension, + QueryResponse::items, QueryResponse::lastEvaluatedKey); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadBatch.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadBatch.java index fdb14c46d66a..6a080072abef 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadBatch.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadBatch.java @@ -26,29 +26,29 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.extensions.dynamodb.mappingclient.BatchableReadOperation; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTableResource; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; @SdkPublicApi public class ReadBatch { - private final MappedTable mappedTable; + private final MappedTableResource mappedTableResource; private final Collection readOperations; - private ReadBatch(MappedTable mappedTable, Collection readOperations) { - this.mappedTable = mappedTable; + private ReadBatch(MappedTableResource mappedTableResource, Collection readOperations) { + this.mappedTableResource = mappedTableResource; this.readOperations = readOperations; } - public static ReadBatch of(MappedTable mappedTable, + public static ReadBatch of(MappedTableResource mappedTableResource, Collection readOperations) { - return new ReadBatch<>(mappedTable, readOperations); + return new ReadBatch<>(mappedTableResource, readOperations); } - public static ReadBatch of(MappedTable mappedTable, + public static ReadBatch of(MappedTableResource mappedTableResource, BatchableReadOperation... readOperations) { - return new ReadBatch<>(mappedTable, Arrays.asList(readOperations)); + return new ReadBatch<>(mappedTableResource, Arrays.asList(readOperations)); } void addReadRequestsToMap(Map readRequestMap) { @@ -67,7 +67,7 @@ void addReadRequestsToMap(Map readRequestMap) { } String tableName() { - return mappedTable.tableName(); + return mappedTableResource.tableName(); } private KeysAndAttributes generateKeysAndAttributes() { @@ -91,7 +91,7 @@ private KeysAndAttributes generateKeysAndAttributes() { } }) .map(BatchableReadOperation::key) - .map(key -> key.keyMap(mappedTable.tableSchema(), + .map(key -> key.keyMap(mappedTableResource.tableSchema(), TableMetadata.primaryIndexName())) .collect(Collectors.toList()); @@ -101,8 +101,8 @@ private KeysAndAttributes generateKeysAndAttributes() { .build(); } - public MappedTable mappedTable() { - return mappedTable; + public MappedTableResource mappedTableResource() { + return mappedTableResource; } public Collection readOperations() { @@ -120,7 +120,9 @@ public boolean equals(Object o) { ReadBatch readBatch = (ReadBatch) o; - if (mappedTable != null ? ! mappedTable.equals(readBatch.mappedTable) : readBatch.mappedTable != null) { + if (mappedTableResource != null ? !mappedTableResource.equals(readBatch.mappedTableResource) : + readBatch.mappedTableResource != null) { + return false; } return readOperations != null ? readOperations.equals(readBatch.readOperations) : readBatch.readOperations == null; @@ -128,7 +130,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = mappedTable != null ? mappedTable.hashCode() : 0; + int result = mappedTableResource != null ? mappedTableResource.hashCode() : 0; result = 31 * result + (readOperations != null ? readOperations.hashCode() : 0); return result; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadTransaction.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadTransaction.java index 8e1d53c6ad73..88980cf70be3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadTransaction.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ReadTransaction.java @@ -16,27 +16,29 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient.operations; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTableResource; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TransactableReadOperation; import software.amazon.awssdk.services.dynamodb.model.TransactGetItem; @SdkPublicApi public class ReadTransaction { - private final MappedTable mappedTable; + private final MappedTableResource mappedTableResource; private final TransactableReadOperation readOperation; - private ReadTransaction(MappedTable mappedTable, TransactableReadOperation readOperation) { - this.mappedTable = mappedTable; + private ReadTransaction(MappedTableResource mappedTableResource, TransactableReadOperation readOperation) { + this.mappedTableResource = mappedTableResource; this.readOperation = readOperation; } - public static ReadTransaction of(MappedTable mappedTable, TransactableReadOperation readOperation) { - return new ReadTransaction<>(mappedTable, readOperation); + public static ReadTransaction of(MappedTableResource mappedTableResource, + TransactableReadOperation readOperation) { + + return new ReadTransaction<>(mappedTableResource, readOperation); } - public MappedTable mappedTable() { - return mappedTable; + public MappedTableResource mappedTableResource() { + return mappedTableResource; } public TransactableReadOperation readOperation() { @@ -44,9 +46,9 @@ public TransactableReadOperation readOperation() { } TransactGetItem generateTransactGetItem() { - return readOperation.generateTransactGetItem(mappedTable.tableSchema(), - OperationContext.of(mappedTable.tableName()), - mappedTable.mapperExtension()); + return readOperation.generateTransactGetItem(mappedTableResource.tableSchema(), + OperationContext.of(mappedTableResource.tableName()), + mappedTableResource.mapperExtension()); } @Override @@ -60,7 +62,9 @@ public boolean equals(Object o) { ReadTransaction that = (ReadTransaction) o; - if (mappedTable != null ? ! mappedTable.equals(that.mappedTable) : that.mappedTable != null) { + if (mappedTableResource != null ? !mappedTableResource.equals(that.mappedTableResource) + : that.mappedTableResource != null) { + return false; } return readOperation != null ? readOperation.equals(that.readOperation) : that.readOperation == null; @@ -68,7 +72,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = mappedTable != null ? mappedTable.hashCode() : 0; + int result = mappedTableResource != null ? mappedTableResource.hashCode() : 0; result = 31 * result + (readOperation != null ? readOperation.hashCode() : 0); return result; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Scan.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Scan.java index e3ffe2585ddc..b10e89d8fae7 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Scan.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/Scan.java @@ -21,24 +21,26 @@ import java.util.function.Function; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.IndexOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Page; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedIndexOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.PaginatedTableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.TransformIterable; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.awssdk.services.dynamodb.model.ScanResponse; -import software.amazon.awssdk.services.dynamodb.paginators.ScanIterable; @SdkPublicApi -public class Scan implements TableOperation>>, - IndexOperation>> { +public class Scan implements PaginatedTableOperation>, + PaginatedIndexOperation> { + private final Map exclusiveStartKey; private final Integer limit; private final Boolean consistentRead; @@ -92,18 +94,29 @@ public ScanRequest generateRequest(TableSchema tableSchema, } @Override - public Iterable> transformResponse(ScanIterable response, - TableSchema tableSchema, - OperationContext operationContext, - MapperExtension mapperExtension) { - return TransformIterable.of(response, getScanResponseIterator(tableSchema, operationContext, mapperExtension)); + public Page transformResponse(ScanResponse response, + TableSchema tableSchema, + OperationContext context, + MapperExtension mapperExtension) { + + return readAndTransformPaginatedItems(response, + tableSchema, + context, + mapperExtension, + ScanResponse::items, + ScanResponse::lastEvaluatedKey); } @Override - public Function serviceCall(DynamoDbClient dynamoDbClient) { + public Function> serviceCall(DynamoDbClient dynamoDbClient) { return dynamoDbClient::scanPaginator; } + @Override + public Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient) { + return dynamoDbAsyncClient::scanPaginator; + } + public Map exclusiveStartKey() { return exclusiveStartKey; } @@ -153,16 +166,6 @@ public int hashCode() { return result; } - private Function> getScanResponseIterator(TableSchema tableSchema, - OperationContext operationContext, - MapperExtension mapperExtension) { - return readAndTransformPaginatedItems(tableSchema, - operationContext, - mapperExtension, - ScanResponse::items, - ScanResponse::lastEvaluatedKey); - } - public static final class Builder { private Map exclusiveStartKey; private Integer limit; diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactGetItems.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactGetItems.java index 4d5b857f4d91..efef07e996c0 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactGetItems.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactGetItems.java @@ -17,12 +17,14 @@ import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.extensions.dynamodb.mappingclient.DatabaseOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.TransactGetItemsRequest; import software.amazon.awssdk.services.dynamodb.model.TransactGetItemsResponse; @@ -67,6 +69,13 @@ public Function serviceCall(D return dynamoDbClient::transactGetItems; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::transactGetItems; + } + @Override public List transformResponse(TransactGetItemsResponse response, MapperExtension mapperExtension) { return response.responses() diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactWriteItems.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactWriteItems.java index e3e35b70423d..3b71870ae910 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactWriteItems.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/TransactWriteItems.java @@ -17,12 +17,14 @@ import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.extensions.dynamodb.mappingclient.DatabaseOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest; @@ -77,6 +79,13 @@ public Function serviceCa return dynamoDbClient::transactWriteItems; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::transactWriteItems; + } + public List writeTransactions() { return writeTransactions; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UnmappedItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UnmappedItem.java index febe0ebb6470..621be9ef1c0b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UnmappedItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UnmappedItem.java @@ -20,7 +20,7 @@ import java.util.Map; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTableResource; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -36,11 +36,11 @@ public static UnmappedItem of(Map itemMap) { return new UnmappedItem(itemMap); } - public T getItem(MappedTable mappedTable) { + public T getItem(MappedTableResource mappedTableResource) { return readAndTransformSingleItem(itemMap, - mappedTable.tableSchema(), - OperationContext.of(mappedTable.tableName()), - mappedTable.mapperExtension()); + mappedTableResource.tableSchema(), + OperationContext.of(mappedTableResource.tableName()), + mappedTableResource.mapperExtension()); } @Override diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UpdateItem.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UpdateItem.java index 685cfdd1a851..4a6f66be1d55 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UpdateItem.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/UpdateItem.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,6 +37,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TransactableWriteOperation; import software.amazon.awssdk.extensions.dynamodb.mappingclient.extensions.WriteModification; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ReturnValue; @@ -176,6 +178,13 @@ public Function serviceCall(DynamoDbClien return dynamoDbClient::updateItem; } + @Override + public Function> asyncServiceCall( + DynamoDbAsyncClient dynamoDbAsyncClient) { + + return dynamoDbAsyncClient::updateItem; + } + @Override public TransactWriteItem generateTransactWriteItem(TableSchema tableSchema, OperationContext operationContext, MapperExtension mapperExtension) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteBatch.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteBatch.java index 79a0239d9ca6..052848b8512a 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteBatch.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteBatch.java @@ -23,33 +23,34 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.extensions.dynamodb.mappingclient.BatchableWriteOperation; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTableResource; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.services.dynamodb.model.WriteRequest; @SdkPublicApi public class WriteBatch { - private final MappedTable mappedTable; + private final MappedTableResource mappedTableResource; private final Collection> writeOperations; - private WriteBatch(MappedTable mappedTable, Collection> writeOperations) { - this.mappedTable = mappedTable; + private WriteBatch(MappedTableResource mappedTableResource, + Collection> writeOperations) { + this.mappedTableResource = mappedTableResource; this.writeOperations = writeOperations; } - public static WriteBatch of(MappedTable mappedTable, + public static WriteBatch of(MappedTableResource mappedTableResource, Collection> writeOperations) { - return new WriteBatch<>(mappedTable, writeOperations); + return new WriteBatch<>(mappedTableResource, writeOperations); } @SafeVarargs - public static WriteBatch of(MappedTable mappedTable, + public static WriteBatch of(MappedTableResource mappedTableResource, BatchableWriteOperation... writeOperations) { - return new WriteBatch<>(mappedTable, Arrays.asList(writeOperations)); + return new WriteBatch<>(mappedTableResource, Arrays.asList(writeOperations)); } - public MappedTable mappedTable() { - return mappedTable; + public MappedTableResource mappedTableResource() { + return mappedTableResource; } public Collection> writeOperations() { @@ -67,7 +68,9 @@ public boolean equals(Object o) { WriteBatch that = (WriteBatch) o; - if (mappedTable != null ? ! mappedTable.equals(that.mappedTable) : that.mappedTable != null) { + if (mappedTableResource != null ? !mappedTableResource.equals(that.mappedTableResource) + : that.mappedTableResource != null) { + return false; } return writeOperations != null ? writeOperations.equals(that.writeOperations) : that.writeOperations == null; @@ -75,7 +78,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = mappedTable != null ? mappedTable.hashCode() : 0; + int result = mappedTableResource != null ? mappedTableResource.hashCode() : 0; result = 31 * result + (writeOperations != null ? writeOperations.hashCode() : 0); return result; } @@ -90,13 +93,13 @@ void addWriteRequestsToMap(Map> writeRequestMap writeRequestsForTable.addAll( writeOperations.stream() .map(writeOperation -> - writeOperation.generateWriteRequest(mappedTable.tableSchema(), - OperationContext.of(mappedTable.tableName()), - mappedTable.mapperExtension())) + writeOperation.generateWriteRequest(mappedTableResource.tableSchema(), + OperationContext.of(mappedTableResource.tableName()), + mappedTableResource.mapperExtension())) .collect(Collectors.toList())); } String getTableName() { - return mappedTable.tableName(); + return mappedTableResource.tableName(); } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteTransaction.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteTransaction.java index 8a4946dce2d9..b73c6a602b8c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteTransaction.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/WriteTransaction.java @@ -16,7 +16,7 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient.operations; import software.amazon.awssdk.annotations.SdkPublicApi; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTableResource; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TransactableWriteOperation; import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem; @@ -35,21 +35,21 @@ */ @SdkPublicApi public class WriteTransaction { - private final MappedTable mappedTable; + private final MappedTableResource mappedTableResource; private final TransactableWriteOperation writeOperation; - private WriteTransaction(MappedTable mappedTable, TransactableWriteOperation writeOperation) { - this.mappedTable = mappedTable; + private WriteTransaction(MappedTableResource mappedTableResource, TransactableWriteOperation writeOperation) { + this.mappedTableResource = mappedTableResource; this.writeOperation = writeOperation; } - public static WriteTransaction of(MappedTable mappedTable, + public static WriteTransaction of(MappedTableResource mappedTableResource, TransactableWriteOperation writeOperation) { - return new WriteTransaction<>(mappedTable, writeOperation); + return new WriteTransaction<>(mappedTableResource, writeOperation); } - public MappedTable mappedTable() { - return mappedTable; + public MappedTableResource mappedTableResource() { + return mappedTableResource; } public TransactableWriteOperation writeOperation() { @@ -67,7 +67,9 @@ public boolean equals(Object o) { WriteTransaction that = (WriteTransaction) o; - if (mappedTable != null ? ! mappedTable.equals(that.mappedTable) : that.mappedTable != null) { + if (mappedTableResource != null ? !mappedTableResource.equals(that.mappedTableResource) : + that.mappedTableResource != null) { + return false; } return writeOperation != null ? writeOperation.equals(that.writeOperation) : that.writeOperation == null; @@ -75,14 +77,14 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = mappedTable != null ? mappedTable.hashCode() : 0; + int result = mappedTableResource != null ? mappedTableResource.hashCode() : 0; result = 31 * result + (writeOperation != null ? writeOperation.hashCode() : 0); return result; } TransactWriteItem generateRequest() { - return writeOperation.generateTransactWriteItem(mappedTable.tableSchema(), - OperationContext.of(mappedTable.tableName()), - mappedTable.mapperExtension()); + return writeOperation.generateTransactWriteItem(mappedTableResource.tableSchema(), + OperationContext.of(mappedTableResource.tableName()), + mappedTableResource.mapperExtension()); } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperationTest.java index 12dfff38982b..8430e435db30 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/IndexOperationTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import org.junit.Test; @@ -27,6 +28,7 @@ import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @RunWith(MockitoJUnitRunner.class) @@ -75,6 +77,11 @@ public Function serviceCall(DynamoDbClient dynamoDbClient) { return null; } + @Override + public Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient) { + return null; + } + @Override public String transformResponse(String response, TableSchema tableSchema, OperationContext context, MapperExtension mapperExtension) { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperationTest.java index c55acb5f2206..0ee001e21de1 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/TableOperationTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThat; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import org.junit.Test; @@ -27,6 +28,7 @@ import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @RunWith(MockitoJUnitRunner.class) @@ -74,6 +76,11 @@ public Function serviceCall(DynamoDbClient dynamoDbClient) { return null; } + @Override + public Function> asyncServiceCall(DynamoDbAsyncClient dynamoDbAsyncClient) { + return null; + } + @Override public String transformResponse(String response, TableSchema tableSchema, OperationContext context, MapperExtension mapperExtension) { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedDatabaseTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedDatabaseTest.java new file mode 100644 index 000000000000..94e7f4d7336f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedDatabaseTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import software.amazon.awssdk.extensions.dynamodb.mappingclient.DatabaseOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +@RunWith(MockitoJUnitRunner.class) +public class DynamoDbAsyncMappedDatabaseTest { + @Mock + private DynamoDbAsyncClient mockDynamoDbAsyncClient; + @Mock + private MapperExtension mockMapperExtension; + @Mock + private DatabaseOperation mockDatabaseOperation; + @Mock + private TableSchema mockTableSchema; + + @InjectMocks + private DynamoDbAsyncMappedDatabase dynamoDbAsyncMappedDatabase; + + @Test + public void execute() { + when(mockDatabaseOperation.executeAsync(any(), any())).thenReturn(CompletableFuture.completedFuture("test")); + + String result = dynamoDbAsyncMappedDatabase.execute(mockDatabaseOperation).join(); + + assertThat(result, is("test")); + verify(mockDatabaseOperation).executeAsync(mockDynamoDbAsyncClient, mockMapperExtension); + } + + @Test + public void table() { + DynamoDbAsyncMappedTable mappedTable = dynamoDbAsyncMappedDatabase.table("table-name", mockTableSchema); + + assertThat(mappedTable.dynamoDbClient(), is(mockDynamoDbAsyncClient)); + assertThat(mappedTable.mapperExtension(), is(mockMapperExtension)); + assertThat(mappedTable.tableSchema(), is(mockTableSchema)); + assertThat(mappedTable.tableName(), is("table-name")); + } + + @Test + public void builder_minimal() { + DynamoDbAsyncMappedDatabase builtObject = + DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(mockDynamoDbAsyncClient) + .build(); + + assertThat(builtObject.dynamoDbAsyncClient(), is(mockDynamoDbAsyncClient)); + assertThat(builtObject.mapperExtension(), is(nullValue())); + } + + @Test + public void builder_maximal() { + DynamoDbAsyncMappedDatabase builtObject = + DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(mockDynamoDbAsyncClient) + .extendWith(mockMapperExtension) + .build(); + + assertThat(builtObject.dynamoDbAsyncClient(), is(mockDynamoDbAsyncClient)); + assertThat(builtObject.mapperExtension(), is(mockMapperExtension)); + } + + @Test(expected = IllegalArgumentException.class) + public void builder_missingDynamoDbClient() { + DynamoDbAsyncMappedDatabase.builder().extendWith(mockMapperExtension).build(); + } + + @Test(expected = IllegalArgumentException.class) + public void builder_extraExtension() { + DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(mockDynamoDbAsyncClient) + .extendWith(mockMapperExtension) + .extendWith(mock(MapperExtension.class)) + .build(); + } + + @Test + public void toBuilder() { + DynamoDbAsyncMappedDatabase copiedObject = dynamoDbAsyncMappedDatabase.toBuilder().build(); + + assertThat(copiedObject, is(dynamoDbAsyncMappedDatabase)); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedIndexTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedIndexTest.java new file mode 100644 index 000000000000..30089d168e66 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedIndexTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.stringValue; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import software.amazon.awssdk.extensions.dynamodb.mappingclient.IndexOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithIndices; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +@RunWith(MockitoJUnitRunner.class) +public class DynamoDbAsyncMappedIndexTest { + @Mock + private DynamoDbAsyncClient mockDynamoDbAsyncClient; + + @Mock + private IndexOperation mockIndexOperation; + + @Mock + private MapperExtension mockMapperExtension; + + @Test + public void execute() { + FakeItem expectedOutput = FakeItem.createUniqueFakeItem(); + DynamoDbAsyncMappedIndex dynamoDbMappedIndex = + new DynamoDbAsyncMappedIndex<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItem.getTableSchema(), + "test_table", + "test_index"); + when(mockIndexOperation.executeOnSecondaryIndexAsync(any(), any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(expectedOutput)); + + FakeItem actualOutput = dynamoDbMappedIndex.execute(mockIndexOperation).join(); + + assertThat(actualOutput, is(expectedOutput)); + verify(mockIndexOperation).executeOnSecondaryIndexAsync(FakeItem.getTableSchema(), + "test_table", + "test_index", + mockMapperExtension, + mockDynamoDbAsyncClient); + } + + @Test + public void keyFrom_secondaryIndex_partitionAndSort() { + FakeItemWithIndices item = FakeItemWithIndices.createUniqueFakeItemWithIndices(); + DynamoDbAsyncMappedIndex dynamoDbMappedIndex = + new DynamoDbAsyncMappedIndex<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItemWithIndices.getTableSchema(), + "test_table", + "gsi_1"); + + Key key = dynamoDbMappedIndex.keyFrom(item); + + assertThat(key.partitionKeyValue(), is(stringValue(item.getGsiId()))); + assertThat(key.sortKeyValue(), is(Optional.of(stringValue(item.getGsiSort())))); + } + + @Test + public void keyFrom_secondaryIndex_partitionOnly() { + FakeItemWithIndices item = FakeItemWithIndices.createUniqueFakeItemWithIndices(); + DynamoDbAsyncMappedIndex dynamoDbMappedIndex = + new DynamoDbAsyncMappedIndex<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItemWithIndices.getTableSchema(), + "test_table", + "gsi_2"); + + Key key = dynamoDbMappedIndex.keyFrom(item); + + assertThat(key.partitionKeyValue(), is(stringValue(item.getGsiId()))); + assertThat(key.sortKeyValue(), is(Optional.empty())); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedTableTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedTableTest.java new file mode 100644 index 000000000000..6821d4a5f5d6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/core/DynamoDbAsyncMappedTableTest.java @@ -0,0 +1,146 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.stringValue; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableOperation; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithIndices; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +@RunWith(MockitoJUnitRunner.class) +public class DynamoDbAsyncMappedTableTest { + private static final String TABLE_NAME = "table-name"; + + @Mock + private DynamoDbAsyncClient mockDynamoDbAsyncClient; + + @Mock + private MapperExtension mockMapperExtension; + + @Mock + private TableOperation mockTableOperation; + + @Test + public void execute_callsOperationCorrectly() { + FakeItem expectedOutput = FakeItem.createUniqueFakeItem(); + when(mockTableOperation.executeOnPrimaryIndexAsync(any(), any(), any(), any())) + .thenReturn(CompletableFuture.completedFuture(expectedOutput)); + DynamoDbAsyncMappedTable dynamoDbAsyncMappedTable = + new DynamoDbAsyncMappedTable<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItem.getTableSchema(), + TABLE_NAME); + + FakeItem actualOutput = dynamoDbAsyncMappedTable.execute(mockTableOperation).join(); + + assertThat(actualOutput, is(expectedOutput)); + verify(mockTableOperation).executeOnPrimaryIndexAsync(FakeItem.getTableSchema(), + dynamoDbAsyncMappedTable.tableName(), + mockMapperExtension, + mockDynamoDbAsyncClient); + } + + @Test + public void index_constructsCorrectMappedIndex() { + DynamoDbAsyncMappedTable dynamoDbMappedTable = + new DynamoDbAsyncMappedTable<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItemWithIndices.getTableSchema(), + TABLE_NAME); + + DynamoDbAsyncMappedIndex dynamoDbMappedIndex = dynamoDbMappedTable.index("gsi_1"); + + assertThat(dynamoDbMappedIndex.dynamoDbClient(), is(sameInstance(mockDynamoDbAsyncClient))); + assertThat(dynamoDbMappedIndex.mapperExtension(), is(sameInstance(mockMapperExtension))); + assertThat(dynamoDbMappedIndex.tableSchema(), is(sameInstance(FakeItemWithIndices.getTableSchema()))); + assertThat(dynamoDbMappedIndex.indexName(), is("gsi_1")); + } + + @Test(expected = IllegalArgumentException.class) + public void index_invalidIndex_throwsIllegalArgumentException() { + DynamoDbAsyncMappedTable dynamoDbMappedTable = + new DynamoDbAsyncMappedTable<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItemWithIndices.getTableSchema(), + TABLE_NAME); + + dynamoDbMappedTable.index("invalid"); + } + + @Test + public void keyFrom_primaryIndex_partitionAndSort() { + FakeItemWithSort item = FakeItemWithSort.createUniqueFakeItemWithSort(); + DynamoDbAsyncMappedTable dynamoDbMappedIndex = + new DynamoDbAsyncMappedTable<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItemWithSort.getTableSchema(), + "test_table"); + + Key key = dynamoDbMappedIndex.keyFrom(item); + + assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); + assertThat(key.sortKeyValue(), is(Optional.of(stringValue(item.getSort())))); + } + + @Test + public void keyFrom_primaryIndex_partitionOnly() { + FakeItem item = FakeItem.createUniqueFakeItem(); + DynamoDbAsyncMappedTable dynamoDbMappedIndex = + new DynamoDbAsyncMappedTable<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItem.getTableSchema(), + "test_table"); + + Key key = dynamoDbMappedIndex.keyFrom(item); + + assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); + assertThat(key.sortKeyValue(), is(Optional.empty())); + } + + @Test + public void keyFrom_primaryIndex_partitionAndNullSort() { + FakeItemWithSort item = FakeItemWithSort.createUniqueFakeItemWithoutSort(); + DynamoDbAsyncMappedTable dynamoDbMappedIndex = + new DynamoDbAsyncMappedTable<>(mockDynamoDbAsyncClient, + mockMapperExtension, + FakeItemWithSort.getTableSchema(), + "test_table"); + + Key key = dynamoDbMappedIndex.keyFrom(item); + + assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); + assertThat(key.sortKeyValue(), is(Optional.empty())); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicCrudTest.java new file mode 100644 index 000000000000..e17b9fdacc72 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicCrudTest.java @@ -0,0 +1,625 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +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.extensions.dynamodb.mappingclient.AttributeValues.stringValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primarySortKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.secondarySortKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.string; + +import java.util.Objects; +import java.util.concurrent.CompletionException; + +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.extensions.dynamodb.mappingclient.AsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbAsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.CreateTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.DeleteItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.GetItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.GlobalSecondaryIndex; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.PutItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.UpdateItem; +import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.Projection; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +public class AsyncBasicCrudTest extends LocalDynamoDbAsyncTestBase { + private static class Record { + private String id; + private String sort; + private String attribute; + private String attribute2; + private String attribute3; + + private String getId() { + return id; + } + + private Record setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private Record setSort(String sort) { + this.sort = sort; + return this; + } + + private String getAttribute() { + return attribute; + } + + private Record setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + private String getAttribute2() { + return attribute2; + } + + private Record setAttribute2(String attribute2) { + this.attribute2 = attribute2; + return this; + } + + private String getAttribute3() { + return attribute3; + } + + private Record setAttribute3(String attribute3) { + this.attribute3 = attribute3; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort) && + Objects.equals(attribute, record.attribute) && + Objects.equals(attribute2, record.attribute2) && + Objects.equals(attribute3, record.attribute3); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, attribute, attribute2, attribute3); + } + } + + private static class ShortRecord { + private String id; + private String sort; + private String attribute; + + private String getId() { + return id; + } + + private ShortRecord setId(String id) { + this.id = id; + return this; + } + + private String getSort() { + return sort; + } + + private ShortRecord setSort(String sort) { + this.sort = sort; + return this; + } + + private String getAttribute() { + return attribute; + } + + private ShortRecord setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ShortRecord that = (ShortRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(sort, that.sort) && + Objects.equals(attribute, that.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, attribute); + } + } + + private static final TableSchema TABLE_SCHEMA = + TableSchema.builder() + .newItemSupplier(Record::new) + .attributes( + string("id", Record::getId, Record::setId).as(primaryPartitionKey()), + string("sort", Record::getSort, Record::setSort).as(primarySortKey()), + // This is a DynamoDb reserved word, forces testing of AttributeNames + string("attribute", Record::getAttribute, Record::setAttribute), + // Using tricky characters to force scrubbing of attributeName tokens + string("*attribute2*", Record::getAttribute2, Record::setAttribute2) + .as(secondaryPartitionKey("gsi_1")), + string("attribute3", Record::getAttribute3, Record::setAttribute3) + .as(secondarySortKey("gsi_1"))) + .build(); + + private static final TableSchema SHORT_TABLE_SCHEMA = + TableSchema.builder() + .newItemSupplier(ShortRecord::new) + .attributes( + string("id", ShortRecord::getId, ShortRecord::setId).as(primaryPartitionKey()), + string("sort", ShortRecord::getSort, ShortRecord::setSort).as(primarySortKey()), + string("attribute", ShortRecord::getAttribute, ShortRecord::setAttribute)) + .build(); + + + private AsyncMappedDatabase mappedDatabase = + DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private AsyncMappedTable mappedTable = mappedDatabase.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + private AsyncMappedTable mappedShortTable = mappedDatabase.table(getConcreteTableName("table-name"), + SHORT_TABLE_SCHEMA); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void createTable() { + mappedTable.execute(CreateTable.builder() + .provisionedThroughput(getDefaultProvisionedThroughput()) + .globalSecondaryIndices( + GlobalSecondaryIndex.of("gsi_1", + Projection.builder() + .projectionType(ProjectionType.ALL) + .build(), + getDefaultProvisionedThroughput())) + .build()).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()) + .join(); + } + + @Test + public void putThenGetItemUsingKey() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record result = mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))) + .join(); + + assertThat(result, is(record)); + } + + @Test + public void putThenGetItemUsingKeyItem() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record result = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + + assertThat(result, is(record)); + } + + @Test + public void getNonExistentItem() { + Record result = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void putTwiceThenGetItem() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + + mappedTable.execute(PutItem.of(record2)).join(); + Record result = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + + assertThat(result, is(record2)); + } + + @Test + public void putThenDeleteItem() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record beforeDeleteResult = + mappedTable.execute(DeleteItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + Record afterDeleteResult = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + + assertThat(beforeDeleteResult, is(record)); + assertThat(afterDeleteResult, is(nullValue())); + } + + @Test + public void putWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + mappedTable.execute(PutItem.builder().item(record).conditionExpression(conditionExpression).build()).join(); + + Record result = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + assertThat(result, is(record)); + } + + @Test + public void putWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + mappedTable.execute(PutItem.builder().item(record).conditionExpression(conditionExpression).build()).join(); + } + + @Test + public void deleteNonExistentItem() { + Record result = + mappedTable.execute(DeleteItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + Key key = mappedTable.keyFrom(record); + mappedTable.execute(DeleteItem.builder().key(key).conditionExpression(conditionExpression).build()).join(); + + Record result = mappedTable.execute(GetItem.of(key)).join(); + assertThat(result, is(nullValue())); + } + + @Test + public void deleteWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + mappedTable.execute(DeleteItem.builder().key(mappedTable.keyFrom(record)) + .conditionExpression(conditionExpression) + .build()).join(); + } + + @Test + public void updateOverwriteCompleteRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("five") + .setAttribute3("six"); + Record result = mappedTable.execute(UpdateItem.of(record2)).join(); + + assertThat(result, is(record2)); + } + + @Test + public void updateCreatePartialRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + + Record result = mappedTable.execute(UpdateItem.of(record)).join(); + + assertThat(result, is(record)); + } + + @Test + public void updateCreateKeyOnlyRecord() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value"); + + Record result = mappedTable.execute(UpdateItem.of(record)).join(); + assertThat(result, is(record)); + } + + @Test + public void updateOverwriteModelledNulls() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + Record result = mappedTable.execute(UpdateItem.of(record2)).join(); + + assertThat(result, is(record2)); + } + + @Test + public void updateCanIgnoreNullsAndDoPartialUpdate() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record record2 = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + Record result = mappedTable.execute(UpdateItem.builder().item(record2).ignoreNulls(true).build()).join(); + + Record expectedResult = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("two") + .setAttribute3("three"); + assertThat(result, is(expectedResult)); + } + + @Test + public void updateShortRecordDoesPartialUpdate() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + ShortRecord record2 = new ShortRecord() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four"); + ShortRecord shortResult = mappedShortTable.execute(UpdateItem.of(record2)).join(); + Record result = mappedTable.execute(GetItem.of(Key.of(stringValue(record.getId()), + stringValue(record.getSort())))).join(); + + Record expectedResult = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("four") + .setAttribute2("two") + .setAttribute3("three"); + assertThat(result, is(expectedResult)); + assertThat(shortResult, is(record2)); + } + + @Test + public void updateKeyOnlyExistingRecordDoesNothing() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + Record updateRecord = new Record().setId("id-value").setSort("sort-value"); + + Record result = mappedTable.execute(UpdateItem.builder().item(updateRecord).ignoreNulls(true).build()).join(); + + assertThat(result, is(record)); + } + + @Test + public void updateWithConditionThatSucceeds() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("three")) + .build(); + + mappedTable.execute(UpdateItem.builder().item(record).conditionExpression(conditionExpression).build()).join(); + + Record result = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + assertThat(result, is(record)); + } + + @Test + public void updateWithConditionThatFails() { + Record record = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one") + .setAttribute2("two") + .setAttribute3("three"); + + mappedTable.execute(PutItem.of(record)).join(); + record.setAttribute("four"); + + Expression conditionExpression = Expression.builder() + .expression("#key = :value OR #key1 = :value1") + .putExpressionName("#key", "attribute") + .putExpressionName("#key1", "attribute3") + .putExpressionValue(":value", stringValue("wrong")) + .putExpressionValue(":value1", stringValue("wrong")) + .build(); + + exception.expect(CompletionException.class); + exception.expectCause(instanceOf(ConditionalCheckFailedException.class)); + mappedTable.execute(UpdateItem.builder().item(record).conditionExpression(conditionExpression).build()).join(); + } + + @Test + public void getAShortRecordWithNewModelledFields() { + ShortRecord shortRecord = new ShortRecord() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + mappedShortTable.execute(PutItem.of(shortRecord)).join(); + Record expectedRecord = new Record() + .setId("id-value") + .setSort("sort-value") + .setAttribute("one"); + + Record result = + mappedTable.execute(GetItem.of(Key.of(stringValue("id-value"), stringValue("sort-value")))).join(); + assertThat(result, is(expectedRecord)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicQueryTest.java new file mode 100644 index 000000000000..17013c41fe51 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicQueryTest.java @@ -0,0 +1,266 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +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.extensions.dynamodb.mappingclient.AttributeValues.numberValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.stringValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.QueryConditional.between; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.QueryConditional.equalTo; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primarySortKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.integerNumber; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.string; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Page; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbAsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.CreateTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.PutItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.Query; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class AsyncBasicQueryTest extends LocalDynamoDbAsyncTestBase { + private static class Record { + private String id; + private Integer sort; + private Integer value; + + public String getId() { + return id; + } + + public Record setId(String id) { + this.id = id; + return this; + } + + public Integer getSort() { + return sort; + } + + public Record setSort(Integer sort) { + this.sort = sort; + return this; + } + + public Integer getValue() { + return value; + } + + public Record setValue(Integer value) { + this.value = value; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort) && + Objects.equals(value, record.value); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, value); + } + } + + private static final TableSchema TABLE_SCHEMA = + TableSchema.builder() + .newItemSupplier(Record::new) + .attributes( + string("id", Record::getId, Record::setId).as(primaryPartitionKey()), + integerNumber("sort", Record::getSort, Record::setSort).as(primarySortKey()), + integerNumber("value", Record::getValue, Record::setValue)) + .build(); + + private static final List RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> new Record().setId("id-value").setSort(i).setValue(i)) + .collect(Collectors.toList()); + + private AsyncMappedDatabase mappedDatabase = DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private AsyncMappedTable mappedTable = mappedDatabase.table(getConcreteTableName("table-name"), TABLE_SCHEMA); + + private void insertRecords() { + RECORDS.forEach(record -> mappedTable.execute(PutItem.of(record)).join()); + } + + private static List drainPublisher(SdkPublisher publisher, int expectedNumberOfResults) { + BufferingSubscriber subscriber = new BufferingSubscriber<>(); + publisher.subscribe(subscriber); + subscriber.waitForCompletion(1000L); + + assertThat(subscriber.isCompleted(), is(true)); + assertThat(subscriber.bufferedError(), is(nullValue())); + assertThat(subscriber.bufferedItems().size(), is(expectedNumberOfResults)); + + return subscriber.bufferedItems(); + } + + @Before + public void createTable() { + mappedTable.execute(CreateTable.of(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()) + .join(); + } + + @Test + public void queryAllRecordsDefaultSettings() { + insertRecords(); + + SdkPublisher> publisher = mappedTable.execute(Query.of(equalTo(Key.of(stringValue("id-value"))))); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), is(RECORDS)); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryAllRecordsWithFilter() { + insertRecords(); + 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(); + + SdkPublisher> publisher = + mappedTable.execute(Query.builder() + .queryConditional(equalTo(Key.of(stringValue("id-value")))) + .filterExpression(expression) + .build()); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), + is(RECORDS.stream().filter(r -> r.sort >= 3 && r.sort <= 5).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryBetween() { + insertRecords(); + Key fromKey = Key.of(stringValue("id-value"), numberValue(3)); + Key toKey = Key.of(stringValue("id-value"), numberValue(5)); + SdkPublisher> publisher = mappedTable.execute(Query.of(between(fromKey, toKey))); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), + is(RECORDS.stream().filter(r -> r.sort >= 3 && r.sort <= 5).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryLimit() { + insertRecords(); + SdkPublisher> publisher = + mappedTable.execute(Query.builder() + .queryConditional(equalTo(Key.of(stringValue("id-value")))) + .limit(5) + .build()); + + List> results = drainPublisher(publisher, 3); + Page page1 = results.get(0); + Page page2 = results.get(1); + Page page3 = results.get(2); + + 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(), is(RECORDS.subList(0, 5))); + assertThat(page1.lastEvaluatedKey(), is(expectedLastEvaluatedKey1)); + assertThat(page2.items(), is(RECORDS.subList(5, 10))); + assertThat(page2.lastEvaluatedKey(), is(expectedLastEvaluatedKey2)); + assertThat(page3.items(), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryEmpty() { + SdkPublisher> publisher = + mappedTable.execute(Query.of(equalTo(Key.of(stringValue("id-value"))))); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void queryExclusiveStartKey() { + Map exclusiveStartKey = new HashMap<>(); + exclusiveStartKey.put("id", stringValue("id-value")); + exclusiveStartKey.put("sort", numberValue(7)); + insertRecords(); + SdkPublisher> publisher = + mappedTable.execute(Query.builder() + .queryConditional(equalTo(Key.of(stringValue("id-value")))) + .exclusiveStartKey(exclusiveStartKey) + .build()); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + assertThat(page.items(), is(RECORDS.subList(8, 10))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicScanTest.java new file mode 100644 index 000000000000..07c70a11a26d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncBasicScanTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +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.extensions.dynamodb.mappingclient.AttributeValues.numberValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.stringValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primarySortKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.integerNumber; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.string; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Page; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbAsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.CreateTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.PutItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.Scan; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class AsyncBasicScanTest extends LocalDynamoDbAsyncTestBase { + private static class Record { + private String id; + private Integer sort; + + private String getId() { + return id; + } + + private Record setId(String id) { + this.id = id; + return this; + } + + private Integer getSort() { + return sort; + } + + private Record setSort(Integer sort) { + this.sort = sort; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record record = (Record) o; + return Objects.equals(id, record.id) && + Objects.equals(sort, record.sort); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort); + } + } + + private static final TableSchema TABLE_SCHEMA = + TableSchema.builder() + .newItemSupplier(Record::new) + .attributes( + string("id", Record::getId, Record::setId).as(primaryPartitionKey()), + integerNumber("sort", Record::getSort, Record::setSort).as(primarySortKey())) + .build(); + + private static final List RECORDS = + IntStream.range(0, 10) + .mapToObj(i -> new Record().setId("id-value").setSort(i)) + .collect(Collectors.toList()); + + private AsyncMappedDatabase mappedDatabase = DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private AsyncMappedTable mappedTable = mappedDatabase.table(getConcreteTableName("table-name"), + TABLE_SCHEMA); + + private void insertRecords() { + RECORDS.forEach(record -> mappedTable.execute(PutItem.of(record)).join()); + } + + private static List drainPublisher(SdkPublisher publisher, int expectedNumberOfResults) { + BufferingSubscriber subscriber = new BufferingSubscriber<>(); + publisher.subscribe(subscriber); + subscriber.waitForCompletion(1000L); + + assertThat(subscriber.isCompleted(), is(true)); + assertThat(subscriber.bufferedError(), is(nullValue())); + assertThat(subscriber.bufferedItems().size(), is(expectedNumberOfResults)); + + return subscriber.bufferedItems(); + } + + @Before + public void createTable() { + mappedTable.execute(CreateTable.of(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name")) + .build()).join(); + } + + @Test + public void scanAllRecordsDefaultSettings() { + insertRecords(); + + SdkPublisher> publisher = mappedTable.execute(Scan.create()); + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), is(RECORDS)); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanAllRecordsWithFilter() { + insertRecords(); + 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(); + + SdkPublisher> publisher = mappedTable.execute(Scan.builder().filterExpression(expression).build()); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), + is(RECORDS.stream().filter(r -> r.sort >= 3 && r.sort <= 5).collect(Collectors.toList()))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanLimit() { + insertRecords(); + SdkPublisher> publisher = mappedTable.execute(Scan.builder().limit(5).build()); + + List> results = drainPublisher(publisher, 3); + + Page page1 = results.get(0); + Page page2 = results.get(1); + Page page3 = results.get(2); + + assertThat(page1.items(), is(RECORDS.subList(0, 5))); + assertThat(page1.lastEvaluatedKey(), is(getKeyMap(4))); + assertThat(page2.items(), is(RECORDS.subList(5, 10))); + assertThat(page2.lastEvaluatedKey(), is(getKeyMap(9))); + assertThat(page3.items(), is(empty())); + assertThat(page3.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanEmpty() { + SdkPublisher> publisher = mappedTable.execute(Scan.create()); + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), is(empty())); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + @Test + public void scanExclusiveStartKey() { + insertRecords(); + SdkPublisher> publisher = + mappedTable.execute(Scan.builder().exclusiveStartKey(getKeyMap(7)).build()); + + List> results = drainPublisher(publisher, 1); + Page page = results.get(0); + + assertThat(page.items(), is(RECORDS.subList(8, 10))); + assertThat(page.lastEvaluatedKey(), is(nullValue())); + } + + private Map getKeyMap(int sort) { + Map result = new HashMap<>(); + result.put("id", stringValue("id-value")); + result.put("sort", numberValue(sort)); + return Collections.unmodifiableMap(result); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncTransactGetItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncTransactGetItemsTest.java new file mode 100644 index 000000000000..a0f7c0f85798 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncTransactGetItemsTest.java @@ -0,0 +1,190 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.numberValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.integerNumber; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbAsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.CreateTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.GetItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.PutItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.ReadTransaction; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.TransactGetItems; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.UnmappedItem; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; + +public class AsyncTransactGetItemsTest extends LocalDynamoDbAsyncTestBase { + private static class Record1 { + private Integer id; + + private Integer getId() { + return id; + } + + private Record1 setId(Integer id) { + this.id = id; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record1 record1 = (Record1) o; + return Objects.equals(id, record1.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } + + private static class Record2 { + private Integer id; + + private Integer getId() { + return id; + } + + private Record2 setId(Integer id) { + this.id = id; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record2 record2 = (Record2) o; + return Objects.equals(id, record2.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + } + + private static final TableSchema TABLE_SCHEMA_1 = + TableSchema.builder() + .newItemSupplier(Record1::new) + .attributes( + integerNumber("id_1", Record1::getId, Record1::setId).as(primaryPartitionKey())) + .build(); + + private static final TableSchema TABLE_SCHEMA_2 = + TableSchema.builder() + .newItemSupplier(Record2::new) + .attributes( + integerNumber("id_2", Record2::getId, Record2::setId).as(primaryPartitionKey())) + .build(); + + private AsyncMappedDatabase mappedDatabase = DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private AsyncMappedTable mappedTable1 = mappedDatabase.table(getConcreteTableName("table-name-1"), + TABLE_SCHEMA_1); + private AsyncMappedTable mappedTable2 = mappedDatabase.table(getConcreteTableName("table-name-2"), + TABLE_SCHEMA_2); + + private static final List RECORDS_1 = + IntStream.range(0, 2) + .mapToObj(i -> new Record1().setId(i)) + .collect(Collectors.toList()); + + private static final List RECORDS_2 = + IntStream.range(0, 2) + .mapToObj(i -> new Record2().setId(i)) + .collect(Collectors.toList()); + + @Before + public void createTable() { + mappedTable1.execute(CreateTable.of(getDefaultProvisionedThroughput())).join(); + mappedTable2.execute(CreateTable.of(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-1")) + .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-2")) + .build()).join(); + } + + private void insertRecords() { + RECORDS_1.forEach(record -> mappedTable1.execute(PutItem.of(record)).join()); + RECORDS_2.forEach(record -> mappedTable2.execute(PutItem.of(record)).join()); + } + + @Test + public void getRecordsFromMultipleTables() { + insertRecords(); + + List results = + mappedDatabase.execute(TransactGetItems.of( + ReadTransaction.of(mappedTable1, GetItem.of(Key.of(numberValue(0)))), + ReadTransaction.of(mappedTable2, GetItem.of(Key.of(numberValue(0)))), + ReadTransaction.of(mappedTable2, GetItem.of(Key.of(numberValue(1)))), + ReadTransaction.of(mappedTable1, GetItem.of(Key.of(numberValue(1)))))).join(); + + assertThat(results.size(), is(4)); + assertThat(results.get(0).getItem(mappedTable1), is(RECORDS_1.get(0))); + assertThat(results.get(1).getItem(mappedTable2), is(RECORDS_2.get(0))); + assertThat(results.get(2).getItem(mappedTable2), is(RECORDS_2.get(1))); + assertThat(results.get(3).getItem(mappedTable1), is(RECORDS_1.get(1))); + } + + @Test + public void notFoundRecordReturnsNull() { + insertRecords(); + + List results = + mappedDatabase.execute(TransactGetItems.of( + ReadTransaction.of(mappedTable1, GetItem.of(Key.of(numberValue(0)))), + ReadTransaction.of(mappedTable2, GetItem.of(Key.of(numberValue(0)))), + ReadTransaction.of(mappedTable2, GetItem.of(Key.of(numberValue(5)))), + ReadTransaction.of(mappedTable1, GetItem.of(Key.of(numberValue(1)))))).join(); + + assertThat(results.size(), is(4)); + assertThat(results.get(0).getItem(mappedTable1), is(RECORDS_1.get(0))); + assertThat(results.get(1).getItem(mappedTable2), is(RECORDS_2.get(0))); + assertThat(results.get(2).getItem(mappedTable2), is(nullValue())); + assertThat(results.get(3).getItem(mappedTable1), is(RECORDS_1.get(1))); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncTransactWriteItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncTransactWriteItemsTest.java new file mode 100644 index 000000000000..81d6921d145d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/AsyncTransactWriteItemsTest.java @@ -0,0 +1,360 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +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 org.junit.Assert.fail; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.numberValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.AttributeValues.stringValue; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.AttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.integerNumber; +import static software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.Attributes.string; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.AsyncMappedTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableSchema; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.core.DynamoDbAsyncMappedDatabase; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.ConditionCheck; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.CreateTable; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.DeleteItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.GetItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.PutItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.TransactWriteItems; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.UpdateItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.WriteTransaction; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; + +public class AsyncTransactWriteItemsTest extends LocalDynamoDbAsyncTestBase { + private static class Record1 { + private Integer id; + private String attribute; + + private Integer getId() { + return id; + } + + private Record1 setId(Integer id) { + this.id = id; + return this; + } + + private String getAttribute() { + return attribute; + } + + private Record1 setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record1 record1 = (Record1) o; + return Objects.equals(id, record1.id) && + Objects.equals(attribute, record1.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, attribute); + } + } + + private static class Record2 { + private Integer id; + private String attribute; + + private Integer getId() { + return id; + } + + private Record2 setId(Integer id) { + this.id = id; + return this; + } + + private String getAttribute() { + return attribute; + } + + private Record2 setAttribute(String attribute) { + this.attribute = attribute; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Record2 record2 = (Record2) o; + return Objects.equals(id, record2.id) && + Objects.equals(attribute, record2.attribute); + } + + @Override + public int hashCode() { + return Objects.hash(id, attribute); + } + } + + private static final TableSchema TABLE_SCHEMA_1 = + TableSchema.builder() + .newItemSupplier(Record1::new) + .attributes( + integerNumber("id_1", Record1::getId, Record1::setId).as(primaryPartitionKey()), + string("attribute", Record1::getAttribute, Record1::setAttribute)) + .build(); + + private static final TableSchema TABLE_SCHEMA_2 = + TableSchema.builder() + .newItemSupplier(Record2::new) + .attributes( + integerNumber("id_2", Record2::getId, Record2::setId).as(primaryPartitionKey()), + string("attribute", Record2::getAttribute, Record2::setAttribute)) + .build(); + + private AsyncMappedDatabase mappedDatabase = DynamoDbAsyncMappedDatabase.builder() + .dynamoDbClient(getDynamoDbAsyncClient()) + .build(); + + private AsyncMappedTable mappedTable1 = mappedDatabase.table(getConcreteTableName("table-name-1"), + TABLE_SCHEMA_1); + private AsyncMappedTable mappedTable2 = mappedDatabase.table(getConcreteTableName("table-name-2"), + TABLE_SCHEMA_2); + + private static final List RECORDS_1 = + IntStream.range(0, 2) + .mapToObj(i -> new Record1().setId(i).setAttribute(Integer.toString(i))) + .collect(Collectors.toList()); + + private static final List RECORDS_2 = + IntStream.range(0, 2) + .mapToObj(i -> new Record2().setId(i).setAttribute(Integer.toString(i))) + .collect(Collectors.toList()); + + @Before + public void createTable() { + mappedTable1.execute(CreateTable.of(getDefaultProvisionedThroughput())).join(); + mappedTable2.execute(CreateTable.of(getDefaultProvisionedThroughput())).join(); + } + + @After + public void deleteTable() { + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-1")) + .build()).join(); + getDynamoDbAsyncClient().deleteTable(DeleteTableRequest.builder() + .tableName(getConcreteTableName("table-name-2")) + .build()).join(); + } + + @Test + public void singlePut() { + List writeTransactions = + singletonList(WriteTransaction.of(mappedTable1, PutItem.of(RECORDS_1.get(0)))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + Record1 record = mappedTable1.execute(GetItem.of(Key.of(numberValue(0)))).join(); + assertThat(record, is(RECORDS_1.get(0))); + } + + @Test + public void multiplePut() { + List writeTransactions = + asList(WriteTransaction.of(mappedTable1, PutItem.of(RECORDS_1.get(0))), + WriteTransaction.of(mappedTable2, PutItem.of(RECORDS_2.get(0)))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + Record1 record1 = mappedTable1.execute(GetItem.of(Key.of(numberValue(0)))).join(); + Record2 record2 = mappedTable2.execute(GetItem.of(Key.of(numberValue(0)))).join(); + assertThat(record1, is(RECORDS_1.get(0))); + assertThat(record2, is(RECORDS_2.get(0))); + } + + @Test + public void singleUpdate() { + List writeTransactions = + singletonList(WriteTransaction.of(mappedTable1, UpdateItem.of(RECORDS_1.get(0)))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + Record1 record = mappedTable1.execute(GetItem.of(Key.of(numberValue(0)))).join(); + assertThat(record, is(RECORDS_1.get(0))); + } + + @Test + public void multipleUpdate() { + List writeTransactions = + asList(WriteTransaction.of(mappedTable1, UpdateItem.of(RECORDS_1.get(0))), + WriteTransaction.of(mappedTable2, UpdateItem.of(RECORDS_2.get(0)))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + Record1 record1 = mappedTable1.execute(GetItem.of(Key.of(numberValue(0)))).join(); + Record2 record2 = mappedTable2.execute(GetItem.of(Key.of(numberValue(0)))).join(); + assertThat(record1, is(RECORDS_1.get(0))); + assertThat(record2, is(RECORDS_2.get(0))); + } + + @Test + public void singleDelete() { + mappedTable1.execute(PutItem.of(RECORDS_1.get(0))).join(); + + List writeTransactions = + singletonList(WriteTransaction.of(mappedTable1, DeleteItem.of(Key.of(numberValue(0))))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + Record1 record = mappedTable1.execute(GetItem.of(Key.of(numberValue(0)))).join(); + assertThat(record, is(nullValue())); + } + + @Test + public void multipleDelete() { + mappedTable1.execute(PutItem.of(RECORDS_1.get(0))).join(); + mappedTable2.execute(PutItem.of(RECORDS_2.get(0))).join(); + + List writeTransactions = + asList(WriteTransaction.of(mappedTable1, DeleteItem.of(Key.of(numberValue(0)))), + WriteTransaction.of(mappedTable2, DeleteItem.of(Key.of(numberValue(0))))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + Record1 record1 = mappedTable1.execute(GetItem.of(Key.of(numberValue(0)))).join(); + Record2 record2 = mappedTable2.execute(GetItem.of(Key.of(numberValue(0)))).join(); + assertThat(record1, is(nullValue())); + assertThat(record2, is(nullValue())); + } + + @Test + public void singleConditionCheck() { + mappedTable1.execute(PutItem.of(RECORDS_1.get(0))).join(); + + Expression conditionExpression1 = Expression.builder() + .expression("#attribute = :attribute") + .expressionValues(singletonMap(":attribute", stringValue("0"))) + .expressionNames(singletonMap("#attribute", "attribute")) + .build(); + + Key key1 = Key.of(numberValue(0)); + List writeTransactions = + singletonList(WriteTransaction.of(mappedTable1, ConditionCheck.of(key1, conditionExpression1))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + } + + @Test + public void multiConditionCheck() { + mappedTable1.execute(PutItem.of(RECORDS_1.get(0))).join(); + mappedTable2.execute(PutItem.of(RECORDS_2.get(0))).join(); + + Expression conditionExpression1 = Expression.builder() + .expression("#attribute = :attribute") + .expressionValues(singletonMap(":attribute", stringValue("0"))) + .expressionNames(singletonMap("#attribute", "attribute")) + .build(); + + Key key1 = Key.of(numberValue(0)); + Key key2 = Key.of(numberValue(0)); + + List writeTransactions = + asList(WriteTransaction.of(mappedTable1, ConditionCheck.of(key1, conditionExpression1)), + WriteTransaction.of(mappedTable2, ConditionCheck.of(key2, conditionExpression1))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + } + + @Test + public void mixedCommands() { + mappedTable1.execute(PutItem.of(RECORDS_1.get(0))).join(); + mappedTable2.execute(PutItem.of(RECORDS_2.get(0))).join(); + + Expression conditionExpression1 = Expression.builder() + .expression("#attribute = :attribute") + .expressionValues(singletonMap(":attribute", stringValue("0"))) + .expressionNames(singletonMap("#attribute", "attribute")) + .build(); + + Key key1 = Key.of(numberValue(0)); + + List writeTransactions = + asList(WriteTransaction.of(mappedTable1, ConditionCheck.of(key1, conditionExpression1)), + WriteTransaction.of(mappedTable2, PutItem.of(RECORDS_2.get(1))), + WriteTransaction.of(mappedTable1, UpdateItem.of(RECORDS_1.get(1))), + WriteTransaction.of(mappedTable2, DeleteItem.of(Key.of(numberValue(0))))); + + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + + assertThat(mappedTable1.execute(GetItem.of(Key.of(numberValue(1)))).join(), is(RECORDS_1.get(1))); + assertThat(mappedTable2.execute(GetItem.of(Key.of(numberValue(0)))).join(), is(nullValue())); + assertThat(mappedTable2.execute(GetItem.of(Key.of(numberValue(1)))).join(), is(RECORDS_2.get(1))); + } + + @Test + public void mixedCommands_conditionCheckFailsTransaction() { + mappedTable1.execute(PutItem.of(RECORDS_1.get(0))).join(); + mappedTable2.execute(PutItem.of(RECORDS_2.get(0))).join(); + + Expression conditionExpression1 = Expression.builder() + .expression("#attribute = :attribute") + .expressionValues(singletonMap(":attribute", stringValue("1"))) + .expressionNames(singletonMap("#attribute", "attribute")) + .build(); + + Key key1 = Key.of(numberValue(0)); + + List writeTransactions = + asList(WriteTransaction.of(mappedTable2, PutItem.of(RECORDS_2.get(1))), + WriteTransaction.of(mappedTable1, UpdateItem.of(RECORDS_1.get(1))), + WriteTransaction.of(mappedTable1, ConditionCheck.of(key1, conditionExpression1)), + WriteTransaction.of(mappedTable2, DeleteItem.of(Key.of(numberValue(0))))); + + try { + mappedDatabase.execute(TransactWriteItems.of(writeTransactions)).join(); + fail("Expected CompletionException to be thrown"); + } catch (CompletionException e) { + assertThat(e.getCause(), instanceOf(TransactionCanceledException.class)); + } + + assertThat(mappedTable1.execute(GetItem.of(Key.of(numberValue(1)))).join(), is(nullValue())); + assertThat(mappedTable2.execute(GetItem.of(Key.of(numberValue(0)))).join(), is(RECORDS_2.get(0))); + assertThat(mappedTable2.execute(GetItem.of(Key.of(numberValue(1)))).join(), is(nullValue())); + } +} + diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicCrudTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicCrudTest.java index aa4d65854a99..5d1b45ad9467 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicCrudTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicCrudTest.java @@ -49,7 +49,7 @@ import software.amazon.awssdk.services.dynamodb.model.Projection; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; -public class BasicCrudTest extends LocalDynamoDbTestBase { +public class BasicCrudTest extends LocalDynamoDbSyncTestBase { private static class Record { private String id; private String sort; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicQueryTest.java index 9ac8a062092f..441a202dccd6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicQueryTest.java @@ -53,7 +53,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -public class BasicQueryTest extends LocalDynamoDbTestBase { +public class BasicQueryTest extends LocalDynamoDbSyncTestBase { private static class Record { private String id; private Integer sort; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicScanTest.java index 9451f428bbb1..78a5efae3e75 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BasicScanTest.java @@ -50,7 +50,7 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -public class BasicScanTest extends LocalDynamoDbTestBase { +public class BasicScanTest extends LocalDynamoDbSyncTestBase { private static class Record { private String id; private Integer sort; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BufferingSubscriber.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BufferingSubscriber.java new file mode 100644 index 000000000000..6fdcd00ce29b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/BufferingSubscriber.java @@ -0,0 +1,73 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class BufferingSubscriber implements Subscriber { + private final CountDownLatch latch = new CountDownLatch(1); + private final List bufferedItems = new ArrayList<>(); + private Throwable bufferedError = null; + private boolean isCompleted = false; + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T t) { + bufferedItems.add(t); + } + + @Override + public void onError(Throwable throwable) { + this.bufferedError = throwable; + this.latch.countDown(); + } + + @Override + public void onComplete() { + this.isCompleted = true; + this.latch.countDown(); + } + + public void waitForCompletion(long timeoutInMillis) { + try { + this.latch.await(timeoutInMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public List bufferedItems() { + return bufferedItems; + } + + public Throwable bufferedError() { + return bufferedError; + } + + public boolean isCompleted() { + return isCompleted; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/FlattenTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/FlattenTest.java index 35df7b5889b2..57256a525f3a 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/FlattenTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/FlattenTest.java @@ -37,7 +37,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.staticmapper.StaticTableSchema; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -public class FlattenTest extends LocalDynamoDbTestBase { +public class FlattenTest extends LocalDynamoDbSyncTestBase { private static class Record { private String id; private Document document; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/IndexQueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/IndexQueryTest.java index ec49b0a5243f..77da458cd6e6 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/IndexQueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/IndexQueryTest.java @@ -57,7 +57,7 @@ import software.amazon.awssdk.services.dynamodb.model.Projection; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; -public class IndexQueryTest extends LocalDynamoDbTestBase { +public class IndexQueryTest extends LocalDynamoDbSyncTestBase { private static class Record { private String id; private Integer sort; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDb.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDb.java index d9fcae00b179..f381849667b9 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDb.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDb.java @@ -24,6 +24,7 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; /** @@ -75,6 +76,16 @@ DynamoDbClient createClient() { .build(); } + DynamoDbAsyncClient createAsyncClient() { + String endpoint = String.format("http://localhost:%d", port); + return DynamoDbAsyncClient.builder() + .endpointOverride(URI.create(endpoint)) + .region(Region.US_EAST_1) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("dummy-key", "dummy-secret"))) + .build(); + } + /** * Stops the local DynamoDb service and frees up resources it is using. */ diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbAsyncTestBase.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbAsyncTestBase.java new file mode 100644 index 000000000000..f09c23174e9c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbAsyncTestBase.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +public class LocalDynamoDbAsyncTestBase extends LocalDynamoDbTestBase { + private DynamoDbAsyncClient dynamoDbAsyncClient = localDynamoDb().createAsyncClient(); + + protected DynamoDbAsyncClient getDynamoDbAsyncClient() { + return dynamoDbAsyncClient; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbSyncTestBase.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbSyncTestBase.java new file mode 100644 index 000000000000..76bd82b1a25a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbSyncTestBase.java @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2020 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.extensions.dynamodb.mappingclient.functionaltests; + +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +public class LocalDynamoDbSyncTestBase extends LocalDynamoDbTestBase { + private DynamoDbClient dynamoDbClient = localDynamoDb().createClient(); + + protected DynamoDbClient getDynamoDbClient() { + return dynamoDbClient; + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbTestBase.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbTestBase.java index d5d509928d70..324a6f2be4aa 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbTestBase.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/LocalDynamoDbTestBase.java @@ -20,7 +20,6 @@ import org.junit.AfterClass; import org.junit.BeforeClass; -import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; public class LocalDynamoDbTestBase { @@ -32,7 +31,6 @@ public class LocalDynamoDbTestBase { .build(); private String uniqueTableSuffix = UUID.randomUUID().toString(); - private DynamoDbClient dynamoDbClient = localDynamoDb.createClient(); @BeforeClass public static void initializeLocalDynamoDb() { @@ -43,16 +41,16 @@ public static void initializeLocalDynamoDb() { public static void stopLocalDynamoDb() { localDynamoDb.stop(); } + + protected static LocalDynamoDb localDynamoDb() { + return localDynamoDb; + } protected String getConcreteTableName(String logicalTableName) { return logicalTableName + "_" + uniqueTableSuffix; } - protected DynamoDbClient getDynamoDbClient() { - return dynamoDbClient; - } - protected ProvisionedThroughput getDefaultProvisionedThroughput() { return DEFAULT_PROVISIONED_THROUGHPUT; } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactGetItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactGetItemsTest.java index 1defc30c760b..6bf79fd46e11 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactGetItemsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactGetItemsTest.java @@ -43,7 +43,7 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.UnmappedItem; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -public class TransactGetItemsTest extends LocalDynamoDbTestBase { +public class TransactGetItemsTest extends LocalDynamoDbSyncTestBase { private static class Record1 { private Integer id; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactWriteItemsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactWriteItemsTest.java index 052f660dc097..bd6045890fec 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactWriteItemsTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/TransactWriteItemsTest.java @@ -53,7 +53,7 @@ import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException; -public class TransactWriteItemsTest extends LocalDynamoDbTestBase { +public class TransactWriteItemsTest extends LocalDynamoDbSyncTestBase { private static class Record1 { private Integer id; private String attribute; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/VersionedRecordTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/VersionedRecordTest.java index b8722a8253c7..e38a68f5231f 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/VersionedRecordTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/functionaltests/VersionedRecordTest.java @@ -44,7 +44,7 @@ import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException; import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; -public class VersionedRecordTest extends LocalDynamoDbTestBase { +public class VersionedRecordTest extends LocalDynamoDbSyncTestBase { private static class Record { private String id; private String attribute; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItemTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItemTest.java index c6978ce67b8e..b306c9de1d99 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItemTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/BatchGetItemTest.java @@ -39,7 +39,6 @@ import java.util.Arrays; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.IntStream; @@ -50,12 +49,15 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedDatabase; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MappedTable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; import software.amazon.awssdk.extensions.dynamodb.mappingclient.extensions.ReadModification; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; import software.amazon.awssdk.extensions.dynamodb.mappingclient.operations.BatchGetItem.ResultsPage; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -63,8 +65,6 @@ import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes; import software.amazon.awssdk.services.dynamodb.paginators.BatchGetItemIterable; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; @RunWith(MockitoJUnitRunner.class) public class BatchGetItemTest { @@ -119,7 +119,8 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { BatchGetItemIterable expectedResponse = mock(BatchGetItemIterable.class); when(mockDynamoDbClient.batchGetItemPaginator(any(BatchGetItemRequest.class))).thenReturn(expectedResponse); - BatchGetItemIterable response = operation.serviceCall(mockDynamoDbClient).apply(batchGetItemRequest); + SdkIterable response = + operation.serviceCall(mockDynamoDbClient).apply(batchGetItemRequest); assertThat(response, sameInstance(expectedResponse)); verify(mockDynamoDbClient).batchGetItemPaginator(batchGetItemRequest); @@ -204,46 +205,30 @@ public void generateRequest_multipleBatches_multipleTableSchemas_ConflictingCons } @Test - public void transformResponse_multiplePages_multipleTables_multipleItems_noExtension() { - Map>> page1 = new HashMap<>(); - page1.put(TABLE_NAME, Arrays.asList(FAKE_ITEM_MAPS.get(0), FAKE_ITEM_MAPS.get(1))); - page1.put(TABLE_NAME_2, singletonList(FAKESORT_ITEM_MAPS.get(0))); - Map>> page2 = new HashMap<>(); - page2.put(TABLE_NAME_2, Arrays.asList(FAKESORT_ITEM_MAPS.get(1), FAKESORT_ITEM_MAPS.get(2))); - page2.put(TABLE_NAME, singletonList(FAKE_ITEM_MAPS.get(2))); - BatchGetItemIterable fakeResults = generateFakeResults(Arrays.asList(page1, page2)); + public void transformResponse_multipleTables_multipleItems_noExtension() { + Map>> page = new HashMap<>(); + page.put(TABLE_NAME, Arrays.asList(FAKE_ITEM_MAPS.get(0), FAKE_ITEM_MAPS.get(1))); + page.put(TABLE_NAME_2, singletonList(FAKESORT_ITEM_MAPS.get(0))); + + BatchGetItemResponse fakeResults = generateFakeResults(page); BatchGetItem operation = BatchGetItem.of(); - Iterator results = operation.transformResponse(fakeResults, null).iterator(); - - assertThat(results.hasNext(), is(true)); - ResultsPage resultsPage1 = results.next(); - assertThat(results.hasNext(), is(true)); - ResultsPage resultsPage2 = results.next(); - assertThat(results.hasNext(), is(false)); - - List fakeItemResultsPage1 = resultsPage1.getResultsForTable(fakeItemMappedTable); - List fakeItemWithSortResultsPage1 = - resultsPage1.getResultsForTable(fakeItemWithSortMappedTable); - List fakeItemResultsPage2 = resultsPage2.getResultsForTable(fakeItemMappedTable); - List fakeItemWithSortResultsPage2 = - resultsPage2.getResultsForTable(fakeItemWithSortMappedTable); - - assertThat(fakeItemResultsPage1, containsInAnyOrder(FAKE_ITEMS.get(0), FAKE_ITEMS.get(1))); - assertThat(fakeItemResultsPage2, containsInAnyOrder(FAKE_ITEMS.get(2))); - assertThat(fakeItemWithSortResultsPage1, containsInAnyOrder(FAKESORT_ITEMS.get(0))); - assertThat(fakeItemWithSortResultsPage2, containsInAnyOrder(FAKESORT_ITEMS.get(1), FAKESORT_ITEMS.get(2))); + ResultsPage resultsPage = operation.transformResponse(fakeResults, null); + + List fakeItemResultsPage = resultsPage.getResultsForTable(fakeItemMappedTable); + List fakeItemWithSortResultsPage = + resultsPage.getResultsForTable(fakeItemWithSortMappedTable); + + assertThat(fakeItemResultsPage, containsInAnyOrder(FAKE_ITEMS.get(0), FAKE_ITEMS.get(1))); + assertThat(fakeItemWithSortResultsPage, containsInAnyOrder(FAKESORT_ITEMS.get(0))); } @Test - public void transformResponse_multiplePages_multipleTables_multipleItems_extensionWithTransformation() { - Map>> page1 = new HashMap<>(); - page1.put(TABLE_NAME, Arrays.asList(FAKE_ITEM_MAPS.get(0), FAKE_ITEM_MAPS.get(1))); - page1.put(TABLE_NAME_2, singletonList(FAKESORT_ITEM_MAPS.get(0))); - Map>> page2 = new HashMap<>(); - page2.put(TABLE_NAME_2, Arrays.asList(FAKESORT_ITEM_MAPS.get(1), FAKESORT_ITEM_MAPS.get(2))); - page2.put(TABLE_NAME, singletonList(FAKE_ITEM_MAPS.get(2))); - BatchGetItemIterable fakeResults = generateFakeResults(Arrays.asList(page1, page2)); + public void transformResponse_multipleTables_multipleItems_extensionWithTransformation() { + Map>> page = new HashMap<>(); + page.put(TABLE_NAME, Arrays.asList(FAKE_ITEM_MAPS.get(0), FAKE_ITEM_MAPS.get(1))); + page.put(TABLE_NAME_2, singletonList(FAKESORT_ITEM_MAPS.get(0))); + BatchGetItemResponse fakeResults = generateFakeResults(page); BatchGetItem operation = BatchGetItem.of(); // Use the mock extension to transform every item based on table name @@ -261,50 +246,32 @@ public void transformResponse_multiplePages_multipleTables_multipleItems_extensi any(TableMetadata.class)); }); - Iterator results = operation.transformResponse(fakeResults, mockExtension).iterator(); - - assertThat(results.hasNext(), is(true)); - ResultsPage resultsPage1 = results.next(); - assertThat(results.hasNext(), is(true)); - ResultsPage resultsPage2 = results.next(); - assertThat(results.hasNext(), is(false)); - - List fakeItemResultsPage1 = resultsPage1.getResultsForTable(fakeItemMappedTable); - List fakeItemWithSortResultsPage1 = - resultsPage1.getResultsForTable(fakeItemWithSortMappedTable); - List fakeItemResultsPage2 = resultsPage2.getResultsForTable(fakeItemMappedTable); - List fakeItemWithSortResultsPage2 = - resultsPage2.getResultsForTable(fakeItemWithSortMappedTable); - - assertThat(fakeItemResultsPage1, containsInAnyOrder(FAKE_ITEMS.get(3), FAKE_ITEMS.get(4))); - assertThat(fakeItemResultsPage2, containsInAnyOrder(FAKE_ITEMS.get(5))); - assertThat(fakeItemWithSortResultsPage1, containsInAnyOrder(FAKESORT_ITEMS.get(3))); - assertThat(fakeItemWithSortResultsPage2, containsInAnyOrder(FAKESORT_ITEMS.get(4), FAKESORT_ITEMS.get(5))); + ResultsPage resultsPage = operation.transformResponse(fakeResults, mockExtension); + + List fakeItemResultsPage = resultsPage.getResultsForTable(fakeItemMappedTable); + List fakeItemWithSortResultsPage = + resultsPage.getResultsForTable(fakeItemWithSortMappedTable); + + + assertThat(fakeItemResultsPage, containsInAnyOrder(FAKE_ITEMS.get(3), FAKE_ITEMS.get(4))); + assertThat(fakeItemWithSortResultsPage, containsInAnyOrder(FAKESORT_ITEMS.get(3))); } @Test public void transformResponse_queryingEmptyResults() { - BatchGetItemIterable fakeResults = generateFakeResults(singletonList(emptyMap())); + BatchGetItemResponse fakeResults = generateFakeResults(emptyMap()); BatchGetItem operation = BatchGetItem.of(); - Iterator results = operation.transformResponse(fakeResults, null).iterator(); + ResultsPage resultsPage = operation.transformResponse(fakeResults, null); - assertThat(results.hasNext(), is(true)); - ResultsPage resultsPage = results.next(); - assertThat(results.hasNext(), is(false)); assertThat(resultsPage.getResultsForTable(fakeItemMappedTable), is(emptyList())); } - private static BatchGetItemIterable generateFakeResults( - List>>> itemMapsPages) { - - List responses = - itemMapsPages.stream() - .map(page -> BatchGetItemResponse.builder().responses(page).build()) - .collect(toList()); + private static BatchGetItemResponse generateFakeResults( + Map>> itemMapsPage) { - BatchGetItemIterable mockIterable = mock(BatchGetItemIterable.class); - when(mockIterable.iterator()).thenReturn(responses.iterator()); - return mockIterable; + return BatchGetItemResponse.builder() + .responses(itemMapsPage) + .build(); } } \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/QueryTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/QueryTest.java index ea7a44106820..d8c698ff62c4 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/QueryTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/QueryTest.java @@ -15,8 +15,6 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient.operations; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; @@ -35,13 +33,11 @@ import java.util.Arrays; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,6 +46,8 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Key; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; @@ -57,14 +55,16 @@ import software.amazon.awssdk.extensions.dynamodb.mappingclient.Page; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; import software.amazon.awssdk.extensions.dynamodb.mappingclient.extensions.ReadModification; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithIndices; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.QueryRequest; import software.amazon.awssdk.services.dynamodb.model.QueryResponse; import software.amazon.awssdk.services.dynamodb.paginators.QueryIterable; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithIndices; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.services.dynamodb.paginators.QueryPublisher; @RunWith(MockitoJUnitRunner.class) public class QueryTest { @@ -81,6 +81,8 @@ public class QueryTest { @Mock private DynamoDbClient mockDynamoDbClient; @Mock + private DynamoDbAsyncClient mockDynamoDbAsyncClient; + @Mock private QueryConditional mockQueryConditional; @Mock private MapperExtension mockMapperExtension; @@ -91,12 +93,25 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { QueryIterable mockQueryIterable = mock(QueryIterable.class); when(mockDynamoDbClient.queryPaginator(any(QueryRequest.class))).thenReturn(mockQueryIterable); - QueryIterable response = queryOperation.serviceCall(mockDynamoDbClient).apply(queryRequest); + SdkIterable response = queryOperation.serviceCall(mockDynamoDbClient).apply(queryRequest); assertThat(response, is(mockQueryIterable)); verify(mockDynamoDbClient).queryPaginator(queryRequest); } + @Test + public void getAsyncServiceCall_makesTheRightCallAndReturnsResponse() { + QueryRequest queryRequest = QueryRequest.builder().build(); + QueryPublisher mockQueryPublisher = mock(QueryPublisher.class); + when(mockDynamoDbAsyncClient.queryPaginator(any(QueryRequest.class))).thenReturn(mockQueryPublisher); + + SdkPublisher response = + queryOperation.asyncServiceCall(mockDynamoDbAsyncClient).apply(queryRequest); + + assertThat(response, is(mockQueryPublisher)); + verify(mockDynamoDbAsyncClient).queryPaginator(queryRequest); + } + @Test public void generateRequest_nonDefault_usesQueryConditional() { Map keyItemMap = getAttributeValueMap(keyItem); @@ -319,139 +334,79 @@ public void generateRequest_hashAndSortKey_withExclusiveStartKey() { } @Test - public void transformResults_firstPageMultipleItems_iteratesAndReturnsCorrectItems() { + public void transformResults_multipleItems_returnsCorrectItems() { List queryResultItems = generateFakeItemList(); List> queryResultMaps = queryResultItems.stream().map(QueryTest::getAttributeValueMap).collect(toList()); - QueryIterable queryIterable = generateFakeQueryResults(singletonList(queryResultMaps)); + QueryResponse queryResponse = generateFakeQueryResults(queryResultMaps); - Iterable> queryResultPages = queryOperation.transformResponse(queryIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, null); - Iterator> queryResultPageIterator = queryResultPages.iterator(); + Page queryResultPage = queryOperation.transformResponse(queryResponse, + FakeItem.getTableSchema(), + PRIMARY_CONTEXT, + null); - assertThat(queryResultPageIterator.hasNext(), is(true)); - Page page = queryResultPageIterator.next(); - assertThat(queryResultPageIterator.hasNext(), is(false)); - assertThat(page.items(), is(queryResultItems)); + assertThat(queryResultPage.items(), is(queryResultItems)); } @Test - public void transformResults_firstPageMultipleItems_setsLastEvaluatedKey() { + public void transformResults_multipleItems_setsLastEvaluatedKey() { List queryResultItems = generateFakeItemList(); FakeItem lastEvaluatedKey = createUniqueFakeItem(); List> queryResultMaps = queryResultItems.stream().map(QueryTest::getAttributeValueMap).collect(toList()); - QueryIterable queryIterable = generateFakeQueryResults(singletonList(queryResultMaps), + QueryResponse queryResponse = generateFakeQueryResults(queryResultMaps, getAttributeValueMap(lastEvaluatedKey)); - Iterable> queryResultPages = queryOperation.transformResponse(queryIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, null); - Iterator> queryResultPageIterator = queryResultPages.iterator(); - - assertThat(queryResultPageIterator.hasNext(), is(true)); - Page page = queryResultPageIterator.next(); - assertThat(queryResultPageIterator.hasNext(), is(false)); - assertThat(page.items(), is(queryResultItems)); - assertThat(page.lastEvaluatedKey(), is(getAttributeValueMap(lastEvaluatedKey))); - } + Page queryResultPage = queryOperation.transformResponse(queryResponse, + FakeItem.getTableSchema(), + PRIMARY_CONTEXT, + null); - @Test - public void queryItem_twoPagesMultipleItems_iteratesAndReturnsCorrectItems() { - List queryResultItems1 = generateFakeItemList(); - List queryResultItems2 = generateFakeItemList(); - - List> queryResultMaps1 = - queryResultItems1.stream().map(QueryTest::getAttributeValueMap).collect(toList()); - List> queryResultMaps2 = - queryResultItems2.stream().map(QueryTest::getAttributeValueMap).collect(toList()); - - QueryIterable queryIterable = generateFakeQueryResults(asList(queryResultMaps1, queryResultMaps2)); - - Iterable> queryResultPages = queryOperation.transformResponse(queryIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, null); - Iterator> queryResultPageIterator = queryResultPages.iterator(); - - assertThat(queryResultPageIterator.hasNext(), is(true)); - Page page1 = queryResultPageIterator.next(); - assertThat(queryResultPageIterator.hasNext(), is(true)); - Page page2 = queryResultPageIterator.next(); - assertThat(queryResultPageIterator.hasNext(), is(false)); - assertThat(page1.items(), is(queryResultItems1)); - assertThat(page2.items(), is(queryResultItems2)); + assertThat(queryResultPage.lastEvaluatedKey(), is(getAttributeValueMap(lastEvaluatedKey))); } @Test - public void queryItem_withExtension_correctlyTransformsItems() { - List queryResultItems1 = generateFakeItemList(); - List queryResultItems2 = generateFakeItemList(); - List modifiedResultItems1 = generateFakeItemList(); - List modifiedResultItems2 = generateFakeItemList(); + public void queryItem_withExtension_correctlyTransformsItem() { + List queryResultItems = generateFakeItemList(); + List modifiedResultItems = generateFakeItemList(); - List> queryResultMaps1 = - queryResultItems1.stream().map(QueryTest::getAttributeValueMap).collect(toList()); - List> queryResultMaps2 = - queryResultItems2.stream().map(QueryTest::getAttributeValueMap).collect(toList()); + List> queryResultMap = + queryResultItems.stream().map(QueryTest::getAttributeValueMap).collect(toList()); ReadModification[] readModifications = - Stream.concat(modifiedResultItems1.stream(), modifiedResultItems2.stream()) - .map(QueryTest::getAttributeValueMap) - .map(attributeMap -> ReadModification.builder().transformedItem(attributeMap).build()) - .collect(Collectors.toList()) - .toArray(new ReadModification[]{}); + modifiedResultItems.stream() + .map(QueryTest::getAttributeValueMap) + .map(attributeMap -> ReadModification.builder().transformedItem(attributeMap).build()) + .collect(Collectors.toList()) + .toArray(new ReadModification[]{}); + when(mockMapperExtension.afterRead(anyMap(), any(), any())) .thenReturn(readModifications[0], Arrays.copyOfRange(readModifications, 1, readModifications.length)); - QueryIterable queryIterable = generateFakeQueryResults(asList(queryResultMaps1, queryResultMaps2)); - - Iterable> queryResultPages = queryOperation.transformResponse(queryIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, - mockMapperExtension); - Iterator> queryResultPageIterator = queryResultPages.iterator(); + QueryResponse queryResponse = generateFakeQueryResults(queryResultMap); - assertThat(queryResultPageIterator.hasNext(), is(true)); - Page page1 = queryResultPageIterator.next(); - assertThat(queryResultPageIterator.hasNext(), is(true)); - Page page2 = queryResultPageIterator.next(); - assertThat(queryResultPageIterator.hasNext(), is(false)); - assertThat(page1.items(), is(modifiedResultItems1)); - assertThat(page2.items(), is(modifiedResultItems2)); + Page queryResultPage = queryOperation.transformResponse(queryResponse, + FakeItem.getTableSchema(), + PRIMARY_CONTEXT, + mockMapperExtension); + assertThat(queryResultPage.items(), is(modifiedResultItems)); InOrder inOrder = Mockito.inOrder(mockMapperExtension); - Stream.concat(queryResultMaps1.stream(), queryResultMaps2.stream()) - .forEach(attributeMap -> - inOrder.verify(mockMapperExtension).afterRead(attributeMap, - PRIMARY_CONTEXT, - FakeItem.getTableMetadata())); + queryResultMap.forEach( + attributeMap -> inOrder.verify(mockMapperExtension) + .afterRead(attributeMap, PRIMARY_CONTEXT, FakeItem.getTableMetadata())); } - private static QueryIterable generateFakeQueryResults(List>> queryItemMapsPages) { - List queryResponses = - queryItemMapsPages.stream().map(page -> QueryResponse.builder().items(page).build()).collect(toList()); - - QueryIterable mockQueryIterable = mock(QueryIterable.class); - when(mockQueryIterable.iterator()).thenReturn(queryResponses.iterator()); - return mockQueryIterable; + private static QueryResponse generateFakeQueryResults(List> queryItemMapsPage) { + return QueryResponse.builder().items(queryItemMapsPage).build(); } - private static QueryIterable generateFakeQueryResults(List>> queryItemMapsPages, - Map lastEvaluatedKey) { - List queryResponses = - queryItemMapsPages.stream() - .map(page -> QueryResponse.builder() - .items(page) - .lastEvaluatedKey(lastEvaluatedKey) - .build()) - .collect(toList()); + private static QueryResponse generateFakeQueryResults(List> queryItemMapsPage, + Map lastEvaluatedKey) { + return QueryResponse.builder().items(queryItemMapsPage).lastEvaluatedKey(lastEvaluatedKey).build(); - QueryIterable mockQueryIterable = mock(QueryIterable.class); - when(mockQueryIterable.iterator()).thenReturn(queryResponses.iterator()); - return mockQueryIterable; } private static List generateFakeItemList() { diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ScanTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ScanTest.java index bfaf9a798680..0d97104217ae 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ScanTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/extensions/dynamodb/mappingclient/operations/ScanTest.java @@ -15,7 +15,6 @@ package software.amazon.awssdk.extensions.dynamodb.mappingclient.operations; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; @@ -32,12 +31,10 @@ import static software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,20 +43,24 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Expression; import software.amazon.awssdk.extensions.dynamodb.mappingclient.MapperExtension; import software.amazon.awssdk.extensions.dynamodb.mappingclient.OperationContext; import software.amazon.awssdk.extensions.dynamodb.mappingclient.Page; import software.amazon.awssdk.extensions.dynamodb.mappingclient.TableMetadata; import software.amazon.awssdk.extensions.dynamodb.mappingclient.extensions.ReadModification; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithIndices; +import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.awssdk.services.dynamodb.model.ScanResponse; import software.amazon.awssdk.services.dynamodb.paginators.ScanIterable; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItem; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithIndices; -import software.amazon.awssdk.extensions.dynamodb.mappingclient.functionaltests.models.FakeItemWithSort; +import software.amazon.awssdk.services.dynamodb.paginators.ScanPublisher; @RunWith(MockitoJUnitRunner.class) public class ScanTest { @@ -74,6 +75,9 @@ public class ScanTest { @Mock private DynamoDbClient mockDynamoDbClient; + @Mock + private DynamoDbAsyncClient mockDynamoDbAsyncClient; + @Mock private MapperExtension mockMapperExtension; @@ -83,12 +87,25 @@ public void getServiceCall_makesTheRightCallAndReturnsResponse() { ScanIterable mockScanIterable = mock(ScanIterable.class); when(mockDynamoDbClient.scanPaginator(any(ScanRequest.class))).thenReturn(mockScanIterable); - ScanIterable response = scanOperation.serviceCall(mockDynamoDbClient).apply(scanRequest); + SdkIterable response = scanOperation.serviceCall(mockDynamoDbClient).apply(scanRequest); assertThat(response, is(mockScanIterable)); verify(mockDynamoDbClient).scanPaginator(scanRequest); } + @Test + public void getAsyncServiceCall_makesTheRightCallAndReturnsResponse() { + ScanRequest scanRequest = ScanRequest.builder().build(); + ScanPublisher mockScanPublisher = mock(ScanPublisher.class); + when(mockDynamoDbAsyncClient.scanPaginator(any(ScanRequest.class))).thenReturn(mockScanPublisher); + + SdkPublisher response = scanOperation.asyncServiceCall(mockDynamoDbAsyncClient) + .apply(scanRequest); + + assertThat(response, is(mockScanPublisher)); + verify(mockDynamoDbAsyncClient).scanPaginator(scanRequest); + } + @Test public void generateRequest_defaultScan() { ScanRequest request = scanOperation.generateRequest(FakeItem.getTableSchema(), @@ -206,89 +223,47 @@ public void generateRequest_hashAndRangeKey_exclusiveStartKey() { } @Test - public void transformResults_firstPageMultipleItems_iteratesAndReturnsCorrectItems() { + public void transformResults_multipleItems_returnsCorrectItems() { List scanResultItems = generateFakeItemList(); List> scanResultMaps = scanResultItems.stream().map(ScanTest::getAttributeValueMap).collect(toList()); - ScanIterable scanIterable = generateFakeScanResults(singletonList(scanResultMaps)); - - Iterable> scanResultPages = scanOperation.transformResponse(scanIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, - null); - Iterator> scanResultPageIterator = scanResultPages.iterator(); + ScanResponse scanResponse = generateFakeScanResults(scanResultMaps); - assertThat(scanResultPageIterator.hasNext(), is(true)); - Page page = scanResultPageIterator.next(); - assertThat(scanResultPageIterator.hasNext(), is(false)); - assertThat(page.items(), is(scanResultItems)); + Page scanResultPage = scanOperation.transformResponse(scanResponse, + FakeItem.getTableSchema(), + PRIMARY_CONTEXT, + null); + assertThat(scanResultPage.items(), is(scanResultItems)); } @Test - public void transformResults_firstPageMultipleItems_setsLastEvaluatedKey() { + public void transformResults_multipleItems_setsLastEvaluatedKey() { List scanResultItems = generateFakeItemList(); FakeItem lastEvaluatedKey = createUniqueFakeItem(); List> scanResultMaps = scanResultItems.stream().map(ScanTest::getAttributeValueMap).collect(toList()); - ScanIterable scanIterable = generateFakeScanResults(singletonList(scanResultMaps), - getAttributeValueMap(lastEvaluatedKey)); - - Iterable> scanResultPages = scanOperation.transformResponse(scanIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, - null); - Iterator> scanResultPageIterator = scanResultPages.iterator(); + ScanResponse scanResponse = generateFakeScanResults(scanResultMaps, getAttributeValueMap(lastEvaluatedKey)); - assertThat(scanResultPageIterator.hasNext(), is(true)); - Page page = scanResultPageIterator.next(); - assertThat(scanResultPageIterator.hasNext(), is(false)); - assertThat(page.items(), is(scanResultItems)); - assertThat(page.lastEvaluatedKey(), is(getAttributeValueMap(lastEvaluatedKey))); - } + Page scanResultPage = scanOperation.transformResponse(scanResponse, + FakeItem.getTableSchema(), + PRIMARY_CONTEXT, + null); - @Test - public void scanItem_twoPagesMultipleItems_iteratesAndReturnsCorrectItems() { - List scanResultItems1 = generateFakeItemList(); - List scanResultItems2 = generateFakeItemList(); - - List> scanResultMaps1 = - scanResultItems1.stream().map(ScanTest::getAttributeValueMap).collect(toList()); - List> scanResultMaps2 = - scanResultItems2.stream().map(ScanTest::getAttributeValueMap).collect(toList()); - - ScanIterable scanIterable = generateFakeScanResults(asList(scanResultMaps1, scanResultMaps2)); - - Iterable> scanResultPages = scanOperation.transformResponse(scanIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, - null); - Iterator> scanResultPageIterator = scanResultPages.iterator(); - - assertThat(scanResultPageIterator.hasNext(), is(true)); - Page page1 = scanResultPageIterator.next(); - assertThat(scanResultPageIterator.hasNext(), is(true)); - Page page2 = scanResultPageIterator.next(); - assertThat(scanResultPageIterator.hasNext(), is(false)); - assertThat(page1.items(), is(scanResultItems1)); - assertThat(page2.items(), is(scanResultItems2)); + assertThat(scanResultPage.lastEvaluatedKey(), is(getAttributeValueMap(lastEvaluatedKey))); } @Test public void scanItem_withExtension_correctlyTransformsItems() { - List scanResultItems1 = generateFakeItemList(); - List scanResultItems2 = generateFakeItemList(); - List modifiedResultItems1 = generateFakeItemList(); - List modifiedResultItems2 = generateFakeItemList(); + List scanResultItems = generateFakeItemList(); + List modifiedResultItems = generateFakeItemList(); - List> scanResultMaps1 = - scanResultItems1.stream().map(ScanTest::getAttributeValueMap).collect(toList()); - List> scanResultMaps2 = - scanResultItems2.stream().map(ScanTest::getAttributeValueMap).collect(toList()); + List> scanResultMaps = + scanResultItems.stream().map(ScanTest::getAttributeValueMap).collect(toList()); ReadModification[] readModifications = - Stream.concat(modifiedResultItems1.stream(), modifiedResultItems2.stream()) + modifiedResultItems.stream() .map(ScanTest::getAttributeValueMap) .map(attributeMap -> ReadModification.builder().transformedItem(attributeMap).build()) .collect(Collectors.toList()) @@ -296,52 +271,32 @@ public void scanItem_withExtension_correctlyTransformsItems() { when(mockMapperExtension.afterRead(anyMap(), any(), any())) .thenReturn(readModifications[0], Arrays.copyOfRange(readModifications, 1, readModifications.length)); - ScanIterable scanIterable = generateFakeScanResults(asList(scanResultMaps1, scanResultMaps2)); + ScanResponse scanResponse = generateFakeScanResults(scanResultMaps); - Iterable> scanResultPages = scanOperation.transformResponse(scanIterable, - FakeItem.getTableSchema(), - PRIMARY_CONTEXT, - mockMapperExtension); - Iterator> scanResultPageIterator = scanResultPages.iterator(); + Page scanResultPage = scanOperation.transformResponse(scanResponse, + FakeItem.getTableSchema(), + PRIMARY_CONTEXT, + mockMapperExtension); - assertThat(scanResultPageIterator.hasNext(), is(true)); - Page page1 = scanResultPageIterator.next(); - assertThat(scanResultPageIterator.hasNext(), is(true)); - Page page2 = scanResultPageIterator.next(); - assertThat(scanResultPageIterator.hasNext(), is(false)); - assertThat(page1.items(), is(modifiedResultItems1)); - assertThat(page2.items(), is(modifiedResultItems2)); + assertThat(scanResultPage.items(), is(modifiedResultItems)); InOrder inOrder = Mockito.inOrder(mockMapperExtension); - Stream.concat(scanResultMaps1.stream(), scanResultMaps2.stream()) - .forEach(attributeMap -> - inOrder.verify(mockMapperExtension).afterRead(attributeMap, - PRIMARY_CONTEXT, - FakeItem.getTableMetadata())); + scanResultMaps.forEach( + attributeMap -> inOrder.verify(mockMapperExtension).afterRead(attributeMap, + PRIMARY_CONTEXT, + FakeItem.getTableMetadata())); } - private static ScanIterable generateFakeScanResults(List>> scanItemMapsPages) { - List scanResponses = - scanItemMapsPages.stream().map(page -> ScanResponse.builder().items(page).build()).collect(toList()); - - ScanIterable mockScanIterable = mock(ScanIterable.class); - when(mockScanIterable.iterator()).thenReturn(scanResponses.iterator()); - return mockScanIterable; + private static ScanResponse generateFakeScanResults(List> scanItemMapsPage) { + return ScanResponse.builder().items(scanItemMapsPage).build(); } - private static ScanIterable generateFakeScanResults(List>> scanItemMapsPages, - Map lastEvaluatedKey) { - List scanResponses = - scanItemMapsPages.stream() - .map(page -> ScanResponse.builder() - .items(page) - .lastEvaluatedKey(lastEvaluatedKey) - .build()) - .collect(toList()); - - ScanIterable mockScanIterable = mock(ScanIterable.class); - when(mockScanIterable.iterator()).thenReturn(scanResponses.iterator()); - return mockScanIterable; + private static ScanResponse generateFakeScanResults(List> scanItemMapsPage, + Map lastEvaluatedKey) { + return ScanResponse.builder() + .items(scanItemMapsPage) + .lastEvaluatedKey(lastEvaluatedKey) + .build(); } private static List generateFakeItemList() {