diff --git a/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-bd68809.json b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-bd68809.json new file mode 100644 index 000000000000..aa88bc16aab9 --- /dev/null +++ b/.changes/next-release/bugfix-AmazonDynamoDBEnhancedClient-bd68809.json @@ -0,0 +1,6 @@ +{ + "type": "bugfix", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Fixed DynamoDbEnhancedClient DefaultDynamoDbAsyncTable::createTable() to create secondary indices that are defined on annotations of the POJO class, similar to DefaultDynamoDbTable::createTable()." +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/TableIndices.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/TableIndices.java new file mode 100644 index 000000000000..fba5ebf91225 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/TableIndices.java @@ -0,0 +1,65 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.internal; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedLocalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.ProjectionType; + +@SdkInternalApi +public class TableIndices { + private final List indices; + + public TableIndices(List indices) { + this.indices = indices; + } + + public List localSecondaryIndices() { + return Collections.unmodifiableList(indices.stream() + .filter(index -> !TableMetadata.primaryIndexName().equals(index.name())) + .filter(index -> !index.partitionKey().isPresent()) + .map(TableIndices::mapIndexMetadataToEnhancedLocalSecondaryIndex) + .collect(Collectors.toList())); + } + + public List globalSecondaryIndices() { + return Collections.unmodifiableList(indices.stream() + .filter(index -> !TableMetadata.primaryIndexName().equals(index.name())) + .filter(index -> index.partitionKey().isPresent()) + .map(TableIndices::mapIndexMetadataToEnhancedGlobalSecondaryIndex) + .collect(Collectors.toList())); + } + + private static EnhancedLocalSecondaryIndex mapIndexMetadataToEnhancedLocalSecondaryIndex(IndexMetadata indexMetadata) { + return EnhancedLocalSecondaryIndex.builder() + .indexName(indexMetadata.name()) + .projection(pb -> pb.projectionType(ProjectionType.ALL)) + .build(); + } + + private static EnhancedGlobalSecondaryIndex mapIndexMetadataToEnhancedGlobalSecondaryIndex(IndexMetadata indexMetadata) { + return EnhancedGlobalSecondaryIndex.builder() + .indexName(indexMetadata.name()) + .projection(pb -> pb.projectionType(ProjectionType.ALL)) + .build(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java index 1538e977b4c3..cd281dec3d24 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTable.java @@ -17,6 +17,7 @@ import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem; +import java.util.ArrayList; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -25,6 +26,7 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.TableIndices; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteItemOperation; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteTableOperation; @@ -114,7 +116,12 @@ public CompletableFuture createTable(Consumer createTable() { - return createTable(CreateTableEnhancedRequest.builder().build()); + TableIndices indices = new TableIndices(new ArrayList<>(tableSchema.tableMetadata().indices())); + + return createTable(CreateTableEnhancedRequest.builder() + .localSecondaryIndices(indices.localSecondaryIndices()) + .globalSecondaryIndices(indices.globalSecondaryIndices()) + .build()); } @Override diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java index 1bd2638892bd..31ce811b3483 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbTable.java @@ -15,22 +15,17 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.client; -import static java.util.Collections.emptyList; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.createKeyFromItem; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.ArrayList; import java.util.function.Consumer; -import java.util.stream.Collectors; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; -import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; import software.amazon.awssdk.enhanced.dynamodb.Key; -import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.TableIndices; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.CreateTableOperation; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteItemOperation; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DeleteTableOperation; @@ -46,8 +41,6 @@ import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.DeleteItemEnhancedResponse; import software.amazon.awssdk.enhanced.dynamodb.model.DescribeTableEnhancedResponse; -import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; -import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedLocalSecondaryIndex; import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedResponse; import software.amazon.awssdk.enhanced.dynamodb.model.PageIterable; @@ -61,7 +54,6 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse; -import software.amazon.awssdk.services.dynamodb.model.ProjectionType; @SdkInternalApi public class DefaultDynamoDbTable implements DynamoDbTable { @@ -126,52 +118,14 @@ public void createTable(Consumer requestCons @Override public void createTable() { - Map> indexGroups = splitSecondaryIndicesToLocalAndGlobalOnes(); + TableIndices indices = new TableIndices(new ArrayList<>(tableSchema.tableMetadata().indices())); + createTable(CreateTableEnhancedRequest.builder() - .localSecondaryIndices(extractLocalSecondaryIndices(indexGroups)) - .globalSecondaryIndices(extractGlobalSecondaryIndices(indexGroups)) + .localSecondaryIndices(indices.localSecondaryIndices()) + .globalSecondaryIndices(indices.globalSecondaryIndices()) .build()); } - private Map> splitSecondaryIndicesToLocalAndGlobalOnes() { - Collection indices = tableSchema.tableMetadata().indices(); - return indices.stream() - .filter(index -> !TableMetadata.primaryIndexName().equals(index.name())) - .collect(Collectors.groupingBy(metadata -> { - String partitionKeyName = metadata.partitionKey().map(KeyAttributeMetadata::name).orElse(null); - if (partitionKeyName == null) { - return IndexType.LSI; - } - return IndexType.GSI; - })); - } - - private List extractLocalSecondaryIndices(Map> indicesGroups) { - return indicesGroups.getOrDefault(IndexType.LSI, emptyList()).stream() - .map(this::mapIndexMetadataToEnhancedLocalSecondaryIndex) - .collect(Collectors.toList()); - } - - private EnhancedLocalSecondaryIndex mapIndexMetadataToEnhancedLocalSecondaryIndex(IndexMetadata indexMetadata) { - return EnhancedLocalSecondaryIndex.builder() - .indexName(indexMetadata.name()) - .projection(pb -> pb.projectionType(ProjectionType.ALL)) - .build(); - } - - private List extractGlobalSecondaryIndices(Map> indicesGroups) { - return indicesGroups.getOrDefault(IndexType.GSI, emptyList()).stream() - .map(this::mapIndexMetadataToEnhancedGlobalSecondaryIndex) - .collect(Collectors.toList()); - } - - private EnhancedGlobalSecondaryIndex mapIndexMetadataToEnhancedGlobalSecondaryIndex(IndexMetadata indexMetadata) { - return EnhancedGlobalSecondaryIndex.builder() - .indexName(indexMetadata.name()) - .projection(pb -> pb.projectionType(ProjectionType.ALL)) - .build(); - } - @Override public T deleteItem(DeleteItemEnhancedRequest request) { TableOperation> operation = DeleteItemOperation.create(request); diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableIndicesTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableIndicesTest.java new file mode 100644 index 000000000000..fe02468958d1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableIndicesTest.java @@ -0,0 +1,105 @@ +package software.amazon.awssdk.enhanced.dynamodb; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.internal.TableIndices; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticIndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.StaticKeyAttributeMetadata; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; +import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedLocalSecondaryIndex; + +public class TableIndicesTest { + + @Test + public void testLocalSecondaryIndices_onlyIncludesLSIs() { + List indices = Arrays.asList(StaticIndexMetadata.builder() + .name("lsi-1") + .build(), + StaticIndexMetadata.builder() + .name("lsi-2") + .build(), + StaticIndexMetadata.builder() + .name("gsi-1") + .partitionKey(StaticKeyAttributeMetadata.create( + "GlobalIndexPartitionKey", + AttributeValueType.N)) + .build()); + + TableIndices tableIndices = new TableIndices(indices); + + List lsiList = tableIndices.localSecondaryIndices(); + + assertEquals(2, lsiList.size()); + assertTrue(lsiList.stream().anyMatch(i -> "lsi-1".equals(i.indexName()))); + assertTrue(lsiList.stream().anyMatch(i -> "lsi-2".equals(i.indexName()))); + } + + @Test + public void testGlobalSecondaryIndices_onlyIncludesGSIs() { + List indices = Arrays.asList(StaticIndexMetadata.builder() + .name("lsi-1") + .build(), + StaticIndexMetadata.builder() + .name("gsi-1") + .partitionKey(StaticKeyAttributeMetadata.create( + "GlobalIndexPartitionKey1", + AttributeValueType.N)) + .build(), + StaticIndexMetadata.builder() + .name("gsi-2") + .partitionKey(StaticKeyAttributeMetadata.create( + "GlobalIndexPartitionKey2", + AttributeValueType.N)) + .build()); + + TableIndices tableIndices = new TableIndices(indices); + + List gsiList = tableIndices.globalSecondaryIndices(); + + assertEquals(2, gsiList.size()); + assertTrue(gsiList.stream().anyMatch(i -> "gsi-1".equals(i.indexName()))); + assertTrue(gsiList.stream().anyMatch(i -> "gsi-2".equals(i.indexName()))); + } + + @Test + public void testPrimaryIndexIsExcluded() { + List indices = Arrays.asList(StaticIndexMetadata.builder() + .name(TableMetadata.primaryIndexName()) + .partitionKey(StaticKeyAttributeMetadata.create("pk", + AttributeValueType.S)) + .build(), + StaticIndexMetadata.builder() + .name("lsi-1") + .build(), + StaticIndexMetadata.builder() + .name("gsi-1") + .partitionKey(StaticKeyAttributeMetadata.create( + "GlobalIndexPartitionKey", + AttributeValueType.N)) + .build()); + + TableIndices tableIndices = new TableIndices(indices); + + List gsiList = tableIndices.globalSecondaryIndices(); + List lsiList = tableIndices.localSecondaryIndices(); + + assertEquals(1, gsiList.size()); + assertEquals("gsi-1", gsiList.get(0).indexName()); + + assertEquals(1, lsiList.size()); + assertEquals("lsi-1", lsiList.get(0).indexName()); + } + + @Test + public void testEmptyIndexList() { + TableIndices tableIndices = new TableIndices(Collections.emptyList()); + + assertTrue(tableIndices.globalSecondaryIndices().isEmpty()); + assertTrue(tableIndices.localSecondaryIndices().isEmpty()); + } +} diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java index cbf1b7acba56..dd5745b8c048 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/client/DefaultDynamoDbAsyncTableTest.java @@ -16,13 +16,22 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.client; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; 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.enhanced.dynamodb.internal.AttributeValues.stringValue; +import java.util.Iterator; +import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; @@ -31,6 +40,10 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithIndices; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.CreateTableResponse; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex; @RunWith(MockitoJUnitRunner.class) public class DefaultDynamoDbAsyncTableTest { @@ -113,4 +126,55 @@ public void keyFrom_primaryIndex_partitionAndNullSort() { assertThat(key.partitionKeyValue(), is(stringValue(item.getId()))); assertThat(key.sortKeyValue(), is(Optional.empty())); } + + @Test + public void createTable_doesNotTreatPrimaryIndexAsAnyOfSecondaryIndexes() { + DefaultDynamoDbAsyncTable dynamoDbMappedIndex = + new DefaultDynamoDbAsyncTable<>(mockDynamoDbAsyncClient, + mockDynamoDbEnhancedClientExtension, + FakeItem.getTableSchema(), + "test_table"); + + when(mockDynamoDbAsyncClient.createTable(any(CreateTableRequest.class))) + .thenReturn(CompletableFuture.completedFuture(CreateTableResponse.builder().build())); + + dynamoDbMappedIndex.createTable().join(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CreateTableRequest.class); + verify(mockDynamoDbAsyncClient).createTable(requestCaptor.capture()); + + CreateTableRequest request = requestCaptor.getValue(); + + assertThat(request.localSecondaryIndexes().size(), is(0)); + assertThat(request.globalSecondaryIndexes().size(), is(0)); + } + + @Test + public void createTable_groupsSecondaryIndexesExistingInTableSchema() { + DefaultDynamoDbAsyncTable dynamoDbMappedIndex = + new DefaultDynamoDbAsyncTable<>(mockDynamoDbAsyncClient, + mockDynamoDbEnhancedClientExtension, + FakeItemWithIndices.getTableSchema(), + "test_table"); + + when(mockDynamoDbAsyncClient.createTable(any(CreateTableRequest.class))) + .thenReturn(CompletableFuture.completedFuture(CreateTableResponse.builder().build())); + + dynamoDbMappedIndex.createTable().join(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CreateTableRequest.class); + verify(mockDynamoDbAsyncClient).createTable(requestCaptor.capture()); + + CreateTableRequest request = requestCaptor.getValue(); + + assertThat(request.localSecondaryIndexes().size(), is(1)); + Iterator lsiIterator = request.localSecondaryIndexes().iterator(); + assertThat(lsiIterator.next().indexName(), is("lsi_1")); + + assertThat(request.globalSecondaryIndexes().size(), is(2)); + List globalIndicesNames = request.globalSecondaryIndexes().stream() + .map(GlobalSecondaryIndex::indexName) + .collect(Collectors.toList()); + assertThat(globalIndicesNames, containsInAnyOrder("gsi_1", "gsi_2")); + } }