diff --git a/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-57a77ce.json b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-57a77ce.json new file mode 100644 index 000000000000..b419f5a17c67 --- /dev/null +++ b/.changes/next-release/feature-AmazonDynamoDBEnhancedClient-57a77ce.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon DynamoDB Enhanced Client", + "contributor": "", + "description": "Add support for GSI composite key to handle up to 4 partition and 4 sort keys" +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysBeanSchemaIntegrationTest.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysBeanSchemaIntegrationTest.java new file mode 100644 index 000000000000..a8421503baa4 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysBeanSchemaIntegrationTest.java @@ -0,0 +1,35 @@ +/* + * 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; + +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.enhanced.dynamodb.model.CompositeKeyRecord; + +public class QueryGSICompositeKeysBeanSchemaIntegrationTest extends QueryGSICompositeKeysIntegrationTestBase { + + private static final String TABLE_NAME = createTestTableName(); + + @BeforeAll + public static void setup() { + dynamoDbClient = createDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); + mappedTable = enhancedClient.table(TABLE_NAME, TableSchema.fromClass(CompositeKeyRecord.class)); + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); + insertRecords(); + waitForGsiConsistency(); + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysImmutableSchemaIntegrationTest.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysImmutableSchemaIntegrationTest.java new file mode 100644 index 000000000000..36f2f3a9690e --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysImmutableSchemaIntegrationTest.java @@ -0,0 +1,1484 @@ +/* + * 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; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBeginsWith; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortGreaterThan; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortGreaterThanOrEqualTo; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortLessThan; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortLessThanOrEqualTo; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Iterator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.model.ImmutableCompositeKeyRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.ImmutableFlattenedRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +class QueryGSICompositeKeysImmutableSchemaIntegrationTest extends DynamoDbEnhancedIntegrationTestBase { + + private static DynamoDbClient dynamoDbClient; + private static DynamoDbEnhancedClient enhancedClient; + private static DynamoDbTable mappedTable; + + private static final String TABLE_NAME = createTestTableName(); + + @BeforeAll + public static void setup() { + dynamoDbClient = createDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); + mappedTable = enhancedClient.table(TABLE_NAME, TableSchema.fromImmutableClass(ImmutableCompositeKeyRecord.class)); + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); + insertRecords(); + waitForGsiConsistency(); + } + + @AfterAll + public static void teardown() { + try { + dynamoDbClient.deleteTable(r -> r.tableName(mappedTable.tableName())); + } finally { + dynamoDbClient.close(); + } + } + + private static void waitForGsiConsistency() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static final java.util.List COMPOSITE_RECORDS = Arrays.asList( + createRecord("id1", "sort1", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-01T00:00:00Z"), 1, 80.5, "flpk3", "flsk2", Instant.parse("2025-01-01T12:00:00Z")), + createRecord("id2", "sort2", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-06-01T11:21:00Z"), 2, 20.9, "flpk3_b", "flsk2", Instant.parse("2025-06-01T11:21:00Z")), + createRecord("id3", "sort3", "pk1", 200, "pk3", Instant.parse("2025-01-02T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-05T00:00:00Z"), 3, 50.0, "flpk3", "flsk2", Instant.parse("2025-01-05T00:00:00Z")), + createRecord("id4", "sort4", "pk1", 100, "pk3", Instant.parse("2025-07-01T00:10:00Z"), "sk1", "sk2", Instant.parse( + "2025-09-01T05:28:00Z"), 4, 75.3, "flpk3", "flsk2", Instant.parse("2025-09-01T05:28:00Z")), + createRecord("id5", "sort5", "pk1", 100, "pk3", Instant.parse("2025-01-08T05:12:32Z"), "sk1", "sk2", Instant.parse( + "2025-01-02T00:00:00Z"), 1, 50.0, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id6", "sort6", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2_prefix", + Instant.parse("2025-01-01T00:00:00Z"), 1, 60.0, "flpk3", "flsk2_prefix", Instant.parse("2025-01-01T00:00" + + ":00Z")), + createRecord("id7", "sort7", "pk1", 100, "pk3", Instant.parse("2025-06-01T00:00:00Z"), "sk1_prefix", "sk2", + Instant.parse("2025-06-01T10:22:02Z"), 1, 70.0, "flpk3", "flsk2", Instant.parse("2025-06-01T10:22:02Z")), + createRecord("id8", "sort8", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-01T00:00:00Z"), 5, 90.4, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id9", "sort9", "different_pk1", 300, "pk3", Instant.parse("2025-01-03T00:00:00Z"), "sk1", "sk2", + Instant.parse("2025-01-03T20:12:00Z"), 10, 40.2, "flpk3", "flsk2", Instant.parse("2025-01-03T20:12:00Z")), + createRecord("id10", "sort10", "pk1", 100, "pk3", Instant.parse("2025-01-09T12:15:00Z"), "sk1", "sk2_b", Instant.parse( + "2025-02-01T00:00:00Z"), 1, 55.5, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id11", "sort11", "pk1", 100, "pk3", Instant.parse("2025-03-01T16:35:00Z"), "sk1", "sk2_c", Instant.parse( + "2025-03-01T23:15:32Z"), 1, 65.5, "flpk3", "flsk2", Instant.parse("2025-03-01T23:15:32Z")), + createRecord("id12", "sort12", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-03T00:00:00Z"), 1, 85.0, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")) + ); + + private static ImmutableCompositeKeyRecord createRecord(String id, String sort, String pk1, Integer pk2, + String pk3, Instant pk4, String sk1, String sk2, + Instant sk3, Integer sk4, Double flpk2, String flpk3, + String flsk2, Instant flsk3) { + ImmutableFlattenedRecord flattenedRecord = ImmutableFlattenedRecord.builder() + .flpk2(flpk2) + .flpk3(flpk3) + .flsk2(flsk2) + .flsk3(flsk3) + .fldata("fl-data") + .build(); + + return ImmutableCompositeKeyRecord.builder() + .id(id) + .sort(sort) + .pk1(pk1) + .pk2(pk2) + .pk3(pk3) + .pk4(pk4) + .sk1(sk1) + .sk2(sk2) + .sk3(sk3) + .sk4(sk4) + .data("test-data") + .flattenedRecord(flattenedRecord) + .build(); + } + + protected static void insertRecords() { + COMPOSITE_RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record))); + } + + // GSI1: 4 partition keys, 4 sort keys + @Test + void queryGsi1_keyEqualTo_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi1_keyEqualTo_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortBeginsWith_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortBeginsWith_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi1_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortBetween_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1), + k -> k.addPartitionValue("pk1") + .addSortValue(4))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi1_sortBetween_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThan_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(3))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi1_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk0"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThanOrEqual_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortLessThan_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(3))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi1_sortLessThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_b"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortLessThanOrEqual_fourSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi1_sortLessThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi1_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + // GSI2: 3 partition keys, 3 sort keys from the main table and flattened + @Test + void queryGsi2_keyEqualTo_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + } + + @Test + void queryGsi2_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi2_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(80.5) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi2_sortBeginsWith_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi2_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi2_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue("pk1") + .addPartitionValue(75.3) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(3))); + } + + @Test + void queryGsi2_sortBetween_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")), + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-05T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + } + + @Test + void queryGsi2_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(85.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(85.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(11))); + } + + @Test + void queryGsi2_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(90.4) + .addPartitionValue("flpk3") + .addSortValue("sk1"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(90.4) + .addPartitionValue("flpk3") + .addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(7))); + } + + @Test + void queryGsi2_sortGreaterThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(2))); + } + + @Test + void queryGsi2_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi2_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(70.0) + .addPartitionValue("flpk3") + .addSortValue("sk0"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi2_sortGreaterThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + } + + @Test + void queryGsi2_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi2_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(75.3) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(3))); + } + + @Test + void queryGsi2_sortLessThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-05T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi2_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(55.5) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(9))); + } + + @Test + void queryGsi2_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(65.5) + .addPartitionValue("flpk3") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(10))); + } + + @Test + void queryGsi2_sortLessThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi2_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi2_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(90.4) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(7))); + } + + // GSI3: 2 partition keys, 2 sort keys from the main table and flattened, using multiple annotations and different order on + // same attributes + @Test + void queryGsi3_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi3_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi3_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")), + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-06-01T11:21:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2"), + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-06-01T11:21:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi3_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("flsk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysIntegrationTestBase.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysIntegrationTestBase.java new file mode 100644 index 000000000000..3e3a2c70211c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysIntegrationTestBase.java @@ -0,0 +1,2300 @@ +/* + * 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; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBeginsWith; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortBetween; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortGreaterThan; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortGreaterThanOrEqualTo; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortLessThan; +import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.sortLessThanOrEqualTo; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Iterator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.model.CompositeKeyRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.FlattenedRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +abstract class QueryGSICompositeKeysIntegrationTestBase extends DynamoDbEnhancedIntegrationTestBase { + + protected static DynamoDbClient dynamoDbClient; + protected static DynamoDbEnhancedClient enhancedClient; + protected static DynamoDbTable mappedTable; + + @AfterAll + public static void teardown() { + try { + dynamoDbClient.deleteTable(r -> r.tableName(mappedTable.tableName())); + } finally { + dynamoDbClient.close(); + } + } + + private static final java.util.List COMPOSITE_RECORDS = Arrays.asList( + createRecord("id1", "sort1", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-01T00:00:00Z"), 1, 80.5, "flpk3", "flsk2", Instant.parse("2025-01-01T12:00:00Z")), + createRecord("id2", "sort2", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-06-01T11:21:00Z"), 2, 20.9, "flpk3_b", "flsk2", Instant.parse("2025-06-01T11:21:00Z")), + createRecord("id3", "sort3", "pk1", 200, "pk3", Instant.parse("2025-01-02T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-05T00:00:00Z"), 3, 50.0, "flpk3", "flsk2", Instant.parse("2025-01-05T00:00:00Z")), + createRecord("id4", "sort4", "pk1", 100, "pk3", Instant.parse("2025-07-01T00:10:00Z"), "sk1", "sk2", Instant.parse( + "2025-09-01T05:28:00Z"), 4, 75.3, "flpk3", "flsk2", Instant.parse("2025-09-01T05:28:00Z")), + createRecord("id5", "sort5", "pk1", 100, "pk3", Instant.parse("2025-01-08T05:12:32Z"), "sk1", "sk2", Instant.parse( + "2025-01-02T00:00:00Z"), 1, 50.0, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id6", "sort6", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2_prefix", + Instant.parse("2025-01-01T00:00:00Z"), 1, 60.0, "flpk3", "flsk2_prefix", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id7", "sort7", "pk1", 100, "pk3", Instant.parse("2025-06-01T00:00:00Z"), "sk1_prefix", "sk2", + Instant.parse("2025-06-01T10:22:02Z"), 1, 70.0, "flpk3", "flsk2", Instant.parse("2025-06-01T10:22:02Z")), + createRecord("id8", "sort8", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-01T00:00:00Z"), 5, 90.4, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id9", "sort9", "different_pk1", 300, "pk3", Instant.parse("2025-01-03T00:00:00Z"), "sk1", "sk2", + Instant.parse("2025-01-03T20:12:00Z"), 10, 40.2, "flpk3", "flsk2", Instant.parse("2025-01-03T20:12:00Z")), + createRecord("id10", "sort10", "pk1", 100, "pk3", Instant.parse("2025-01-09T12:15:00Z"), "sk1", "sk2_b", Instant.parse( + "2025-02-01T00:00:00Z"), 1, 55.5, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")), + createRecord("id11", "sort11", "pk1", 100, "pk3", Instant.parse("2025-03-01T16:35:00Z"), "sk1", "sk2_c", Instant.parse( + "2025-03-01T23:15:32Z"), 1, 65.5, "flpk3", "flsk2", Instant.parse("2025-03-01T23:15:32Z")), + createRecord("id12", "sort12", "pk1", 100, "pk3", Instant.parse("2025-01-01T00:00:00Z"), "sk1", "sk2", Instant.parse( + "2025-01-03T00:00:00Z"), 1, 85.0, "flpk3", "flsk2", Instant.parse("2025-01-01T00:00:00Z")) + ); + + protected static void insertRecords() { + COMPOSITE_RECORDS.forEach(record -> mappedTable.putItem(r -> r.item(record))); + } + + protected static void waitForGsiConsistency() { + try { + Thread.sleep(3000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static CompositeKeyRecord createRecord(String id, String sort, String pk1, Integer pk2, String pk3, + Instant pk4, String sk1, String sk2, Instant sk3, Integer sk4, + Double flpk2, String flpk3, String flsk2, Instant flsk3) { + CompositeKeyRecord record = new CompositeKeyRecord(); + record.setId(id); + record.setSort(sort); + record.setPk1(pk1); + record.setPk2(pk2); + record.setPk3(pk3); + record.setPk4(pk4); + record.setSk1(sk1); + record.setSk2(sk2); + record.setSk3(sk3); + record.setSk4(sk4); + record.setData("test-data"); + + FlattenedRecord flattenedRecord = new FlattenedRecord(); + flattenedRecord.setFlpk2(flpk2); + flattenedRecord.setFlpk3(flpk3); + flattenedRecord.setFlsk2(flsk2); + flattenedRecord.setFlsk3(flsk3); + flattenedRecord.setFldata("fl-data"); + record.setFlattenedRecord(flattenedRecord); + + return record; + } + + // GSI1: 1 partition key, 1 sort key + @Test + void queryGsi1_keyEqualTo() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(10)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi1_sortBeginsWith() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addSortValue("sk1_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi1_sortBetween() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1").addSortValue("sk1_"), + k -> k.addPartitionValue("pk1").addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + } + + @Test + void queryGsi1_sortGreaterThan() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan(k -> k.addPartitionValue("pk1") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi1_sortGreaterThanOrEqual() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo(k -> k.addPartitionValue("pk1") + .addSortValue("sk1_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi1_sortLessThan() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan(k -> k.addPartitionValue("pk1") + .addSortValue("sk1_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(10)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(false)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(8)), is(false)); + } + + @Test + void queryGsi1_sortLessThanOrEqual() { + Iterator> results = mappedTable.index("gsi1") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo(k -> k.addPartitionValue("pk1") + .addSortValue("sk1_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(11)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(8)), is(false)); + } + + // GSI2: 2 partition keys, 2 sort keys + @Test + void queryGsi2_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(6)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi2_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi2_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi2_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi2_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2_a"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2_d"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi2_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_"), + k -> k.addPartitionValue("pk1") + .addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi2_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi2_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi2_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2_b"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi2_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi2_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(6)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi2_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(9)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(false)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(false)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(8)), is(false)); + } + + @Test + void queryGsi2_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(6)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi2_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi2") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addSortValue("sk1_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(10)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(false)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(8)), is(false)); + } + + // GSI3: 3 partition keys, 3 sort keys + @Test + void queryGsi3_keyEqualTo_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi3_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi3_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi3_sortBeginsWith_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi3_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi3_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi3_sortBetween_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2024-12-31T00:00:00Z")), + k -> k.addPartitionValue("pk1") + .addSortValue(Instant.parse("2025-01-03T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_a"), + k -> k.addPartitionValue("pk1") + .addSortValue("sk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi3_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1_"), + k -> k.addPartitionValue("pk1") + .addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), is(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi3_sortGreaterThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + } + + @Test + void queryGsi3_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(10)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi3_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_b"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(6)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(10)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi3_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1") + .addSortValue("sk2_b"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(7)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi3_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi3") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(9)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + // GSI4: 4 partition keys, 4 sort keys + @Test + void queryGsi4_keyEqualTo_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi4_keyEqualTo_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortBeginsWith_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortBeginsWith_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi4_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortBetween_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1), + k -> k.addPartitionValue("pk1") + .addSortValue(4))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi4_sortBetween_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThan_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan(k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(3))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi4_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk0"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThanOrEqual_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortLessThan_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(3))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi4_sortLessThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_b"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(4)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortLessThanOrEqual_fourSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue(1))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi4_sortLessThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi4_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1") + .addSortValue("sk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi4_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi4") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(100) + .addPartitionValue("pk3") + .addPartitionValue(Instant.parse("2025-01-01T00:00:00Z")) + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(5)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + // GSI5: 3 partition keys, 3 sort keys from the main table and flattened + @Test + void queryGsi5_keyEqualTo_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + } + + @Test + void queryGsi5_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi5_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.addPartitionValue("pk1") + .addPartitionValue(80.5) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(0))); + } + + @Test + void queryGsi5_sortBeginsWith_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi5_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi5_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith(k -> k.addPartitionValue("pk1") + .addPartitionValue(75.3) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(3))); + } + + @Test + void queryGsi5_sortBetween_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")), + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-05T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + } + + @Test + void queryGsi5_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(85.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(85.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(11))); + } + + @Test + void queryGsi5_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue("pk1") + .addPartitionValue(90.4) + .addPartitionValue("flpk3") + .addSortValue("sk1"), + k -> k.addPartitionValue("pk1") + .addPartitionValue(90.4) + .addPartitionValue("flpk3") + .addSortValue("sk1_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(7))); + } + + @Test + void queryGsi5_sortGreaterThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-02T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(2))); + } + + @Test + void queryGsi5_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi5_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(70.0) + .addPartitionValue("flpk3") + .addSortValue("sk0"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(6))); + } + + @Test + void queryGsi5_sortGreaterThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(2)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + } + + @Test + void queryGsi5_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue( + "flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi5_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(75.3) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(3))); + } + + @Test + void queryGsi5_sortLessThan_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-05T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi5_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(55.5) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2_z"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(9))); + } + + @Test + void queryGsi5_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue("pk1") + .addPartitionValue(65.5) + .addPartitionValue("flpk3") + .addSortValue("sk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(10))); + } + + @Test + void queryGsi5_sortLessThanOrEqual_threeSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(50.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(4))); + } + + @Test + void queryGsi5_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(60.0) + .addPartitionValue("flpk3") + .addSortValue("sk1") + .addSortValue( + "flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi5_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi5") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue("pk1") + .addPartitionValue(90.4) + .addPartitionValue("flpk3") + .addSortValue("sk1"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(7))); + } + + // GSI6: 2 partition keys, 2 sort keys from the main table and flattened, using multiple annotations and different order on + // same attributes + @Test + void queryGsi6_keyEqualTo_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi6_keyEqualTo_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi6_sortBeginsWith_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-03T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(11))); + } + + @Test + void queryGsi6_sortBeginsWith_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBeginsWith( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_p"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi6_sortBetween_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")), + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-03-01T11:21:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi6_sortBetween_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortBetween( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_p"), + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_x"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + + } + + @Test + void queryGsi6_sortGreaterThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-05-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + } + + @Test + void queryGsi6_sortGreaterThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThan( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_a"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi6_sortGreaterThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-06-01T10:22:02Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(3)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + } + + @Test + void queryGsi6_sortGreaterThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortGreaterThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_prefix"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(1)); + assertThat(page1.items().get(0), equalTo(COMPOSITE_RECORDS.get(5))); + } + + @Test + void queryGsi6_sortLessThan_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-03-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(5)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi6_sortLessThan_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThan( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2_"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(9)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } + + @Test + void queryGsi6_sortLessThanOrEqual_twoSortKeys() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2") + .addSortValue(Instant.parse("2025-01-01T00:00:00Z")))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(2)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + } + + @Test + void queryGsi6_sortLessThanOrEqual_oneSortKey() { + Iterator> results = mappedTable.index("gsi6") + .query(QueryEnhancedRequest.builder() + .queryConditional(sortLessThanOrEqualTo( + k -> k.addPartitionValue(100) + .addPartitionValue("pk3") + .addSortValue("flsk2"))) + .build()) + .iterator(); + + Page page1 = results.next(); + assertThat(results.hasNext(), is(false)); + assertThat(page1.items(), hasSize(9)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(0)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(1)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(3)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(4)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(6)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(7)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(9)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(10)), is(true)); + assertThat(page1.items().contains(COMPOSITE_RECORDS.get(11)), is(true)); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysStaticSchemaIntegrationTest.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysStaticSchemaIntegrationTest.java new file mode 100644 index 000000000000..e8ea929882cb --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/QueryGSICompositeKeysStaticSchemaIntegrationTest.java @@ -0,0 +1,136 @@ +/* + * 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; + +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import org.junit.jupiter.api.BeforeAll; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.CompositeKeyRecord; +import software.amazon.awssdk.enhanced.dynamodb.model.FlattenedRecord; + +public class QueryGSICompositeKeysStaticSchemaIntegrationTest extends QueryGSICompositeKeysIntegrationTestBase { + + private static final String TABLE_NAME = createTestTableName(); + + private static final StaticTableSchema FLATTENED_RECORD_SCHEMA = + StaticTableSchema.builder(FlattenedRecord.class) + .newItemSupplier(FlattenedRecord::new) + .addAttribute(Double.class, a -> a.name("flpk2") + .getter(FlattenedRecord::getFlpk2) + .setter(FlattenedRecord::setFlpk2) + .tags(secondaryPartitionKey("gsi5", Order.SECOND))) + .addAttribute(String.class, a -> a.name("flpk3") + .getter(FlattenedRecord::getFlpk3) + .setter(FlattenedRecord::setFlpk3) + .tags(secondaryPartitionKey("gsi5", Order.THIRD))) + .addAttribute(String.class, a -> a.name("flsk2") + .getter(FlattenedRecord::getFlsk2) + .setter(FlattenedRecord::setFlsk2) + .tags(secondarySortKey("gsi5", Order.SECOND), + secondarySortKey("gsi6", Order.FIRST))) + .addAttribute(java.time.Instant.class, a -> a.name("flsk3") + .getter(FlattenedRecord::getFlsk3) + .setter(FlattenedRecord::setFlsk3) + .tags(secondarySortKey("gsi5", Order.THIRD))) + .addAttribute(String.class, a -> a.name("fldata") + .getter(FlattenedRecord::getFldata) + .setter(FlattenedRecord::setFldata)) + .build(ExecutionContext.FLATTENED); + + private static final TableSchema COMPOSITE_KEY_SCHEMA = + StaticTableSchema.builder(CompositeKeyRecord.class) + .newItemSupplier(CompositeKeyRecord::new) + .addAttribute(String.class, a -> a.name("id") + .getter(CompositeKeyRecord::getId) + .setter(CompositeKeyRecord::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(CompositeKeyRecord::getSort) + .setter(CompositeKeyRecord::setSort) + .tags(primarySortKey())) + .addAttribute(String.class, a -> a.name("pk1") + .getter(CompositeKeyRecord::getPk1) + .setter(CompositeKeyRecord::setPk1) + .tags(secondaryPartitionKey("gsi1", Order.FIRST), + secondaryPartitionKey("gsi2", Order.FIRST), + secondaryPartitionKey("gsi3", Order.FIRST), + secondaryPartitionKey("gsi4", Order.FIRST), + secondaryPartitionKey("gsi5", Order.FIRST))) + .addAttribute(Integer.class, a -> a.name("pk2") + .getter(CompositeKeyRecord::getPk2) + .setter(CompositeKeyRecord::setPk2) + .tags(secondaryPartitionKey("gsi2", Order.SECOND), + secondaryPartitionKey("gsi3", Order.SECOND), + secondaryPartitionKey("gsi4", Order.SECOND), + secondaryPartitionKey("gsi6", Order.FIRST))) + .addAttribute(String.class, a -> a.name("pk3") + .getter(CompositeKeyRecord::getPk3) + .setter(CompositeKeyRecord::setPk3) + .tags(secondaryPartitionKey("gsi3", Order.THIRD), + secondaryPartitionKey("gsi4", Order.THIRD), + secondaryPartitionKey("gsi6", Order.SECOND))) + .addAttribute(java.time.Instant.class, a -> a.name("pk4") + .getter(CompositeKeyRecord::getPk4) + .setter(CompositeKeyRecord::setPk4) + .tags(secondaryPartitionKey("gsi4", Order.FOURTH))) + .addAttribute(String.class, a -> a.name("sk1") + .getter(CompositeKeyRecord::getSk1) + .setter(CompositeKeyRecord::setSk1) + .tags(secondarySortKey("gsi1", Order.FIRST), + secondarySortKey("gsi2", Order.FIRST), + secondarySortKey("gsi3", Order.FIRST), + secondarySortKey("gsi4", Order.FIRST), + secondarySortKey("gsi5", Order.FIRST))) + .addAttribute(String.class, a -> a.name("sk2") + .getter(CompositeKeyRecord::getSk2) + .setter(CompositeKeyRecord::setSk2) + .tags(secondarySortKey("gsi2", Order.SECOND), + secondarySortKey("gsi3", Order.SECOND), + secondarySortKey("gsi4", Order.SECOND))) + .addAttribute(java.time.Instant.class, a -> a.name("sk3") + .getter(CompositeKeyRecord::getSk3) + .setter(CompositeKeyRecord::setSk3) + .tags(secondarySortKey("gsi3", Order.THIRD), + secondarySortKey("gsi4", Order.THIRD), + secondarySortKey("gsi6", Order.SECOND))) + .addAttribute(Integer.class, a -> a.name("sk4") + .getter(CompositeKeyRecord::getSk4) + .setter(CompositeKeyRecord::setSk4) + .tags(secondarySortKey("gsi4", Order.FOURTH))) + .addAttribute(String.class, a -> a.name("data") + .getter(CompositeKeyRecord::getData) + .setter(CompositeKeyRecord::setData)) + .flatten(FLATTENED_RECORD_SCHEMA, CompositeKeyRecord::getFlattenedRecord, + CompositeKeyRecord::setFlattenedRecord) + .build(); + + @BeforeAll + public static void setup() { + dynamoDbClient = createDynamoDbClient(); + enhancedClient = DynamoDbEnhancedClient.builder().dynamoDbClient(dynamoDbClient).build(); + mappedTable = enhancedClient.table(TABLE_NAME, COMPOSITE_KEY_SCHEMA); + mappedTable.createTable(); + dynamoDbClient.waiter().waitUntilTableExists(r -> r.tableName(TABLE_NAME)); + insertRecords(); + waitForGsiConsistency(); + } + +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/CompositeKeyRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/CompositeKeyRecord.java new file mode 100644 index 000000000000..b82872ea872b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/CompositeKeyRecord.java @@ -0,0 +1,180 @@ +/* + * 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.model; + +import java.time.Instant; +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class CompositeKeyRecord { + private String id; + private String sort; + private String pk1; + private Integer pk2; + private String pk3; + private Instant pk4; + private String sk1; + private String sk2; + private Instant sk3; + private Integer sk4; + private String data; + private FlattenedRecord flattenedRecord; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"gsi1", "gsi2", "gsi3", "gsi4", "gsi5"}, order = Order.FIRST) + public String getPk1() { + return pk1; + } + + public void setPk1(String pk1) { + this.pk1 = pk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"gsi2", "gsi3", "gsi4"}, order = Order.SECOND) + @DynamoDbSecondaryPartitionKey(indexNames = "gsi6", order = Order.FIRST) + public Integer getPk2() { + return pk2; + } + + public void setPk2(Integer pk2) { + this.pk2 = pk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"gsi3", "gsi4"}, order = Order.THIRD) + @DynamoDbSecondaryPartitionKey(indexNames = "gsi6", order = Order.SECOND) + public String getPk3() { + return pk3; + } + + public void setPk3(String pk3) { + this.pk3 = pk3; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi4", order = Order.FOURTH) + public Instant getPk4() { + return pk4; + } + + public void setPk4(Instant pk4) { + this.pk4 = pk4; + } + + @DynamoDbSecondarySortKey(indexNames = {"gsi1", "gsi2", "gsi3", "gsi4", "gsi5"}, order = Order.FIRST) + public String getSk1() { + return sk1; + } + + public void setSk1(String sk1) { + this.sk1 = sk1; + } + + @DynamoDbSecondarySortKey(indexNames = {"gsi2", "gsi3", "gsi4"}, order = Order.SECOND) + public String getSk2() { + return sk2; + } + + public void setSk2(String sk2) { + this.sk2 = sk2; + } + + @DynamoDbSecondarySortKey(indexNames = {"gsi3", "gsi4"}, order = Order.THIRD) + @DynamoDbSecondarySortKey(indexNames = "gsi6", order = Order.SECOND) + public Instant getSk3() { + return sk3; + } + + public void setSk3(Instant sk3) { + this.sk3 = sk3; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi4", order = Order.FOURTH) + public Integer getSk4() { + return sk4; + } + + public void setSk4(Integer sk4) { + this.sk4 = sk4; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + @DynamoDbFlatten + public FlattenedRecord getFlattenedRecord() { + return flattenedRecord; + } + + public void setFlattenedRecord(FlattenedRecord flattenedRecord) { + this.flattenedRecord = flattenedRecord; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompositeKeyRecord that = (CompositeKeyRecord) o; + return Objects.equals(id, that.id) && + Objects.equals(sort, that.sort) && + Objects.equals(pk1, that.pk1) && + Objects.equals(pk2, that.pk2) && + Objects.equals(pk3, that.pk3) && + Objects.equals(pk4, that.pk4) && + Objects.equals(sk1, that.sk1) && + Objects.equals(sk2, that.sk2) && + Objects.equals(sk3, that.sk3) && + Objects.equals(sk4, that.sk4) && + Objects.equals(data, that.data) && + Objects.equals(flattenedRecord, that.flattenedRecord); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, pk1, pk2, pk3, pk4, sk1, sk2, sk3, sk4, data, flattenedRecord); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/FlattenedRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/FlattenedRecord.java new file mode 100644 index 000000000000..15d19f4e570b --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/FlattenedRecord.java @@ -0,0 +1,98 @@ +/* + * 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.model; + +import java.time.Instant; +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbBean +public class FlattenedRecord { + private Double flpk2; + private String flpk3; + private String flsk2; + private Instant flsk3; + private String fldata; + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi5", order = Order.SECOND) + public Double getFlpk2() { + return flpk2; + } + + public void setFlpk2(Double flpk2) { + this.flpk2 = flpk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi5", order = Order.THIRD) + public String getFlpk3() { + return flpk3; + } + + public void setFlpk3(String flpk3) { + this.flpk3 = flpk3; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi5", order = Order.SECOND) + @DynamoDbSecondarySortKey(indexNames = "gsi6", order = Order.FIRST) + public String getFlsk2() { + return flsk2; + } + + public void setFlsk2(String flsk2) { + this.flsk2 = flsk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi5", order = Order.THIRD) + public Instant getFlsk3() { + return flsk3; + } + + public void setFlsk3(Instant flsk3) { + this.flsk3 = flsk3; + } + + public String getFldata() { + return fldata; + } + + public void setFldata(String fldata) { + this.fldata = fldata; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FlattenedRecord that = (FlattenedRecord) o; + return Objects.equals(flpk2, that.flpk2) && + Objects.equals(flpk3, that.flpk3) && + Objects.equals(flsk2, that.flsk2) && + Objects.equals(flsk3, that.flsk3) && + Objects.equals(fldata, that.fldata); + } + + @Override + public int hashCode() { + return Objects.hash(flpk2, flpk3, flsk2, flsk3, fldata); + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/ImmutableCompositeKeyRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/ImmutableCompositeKeyRecord.java new file mode 100644 index 000000000000..eaf745f6ac0f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/ImmutableCompositeKeyRecord.java @@ -0,0 +1,232 @@ +/* + * 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.model; + +import java.time.Instant; +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbImmutable(builder = ImmutableCompositeKeyRecord.Builder.class) +public class ImmutableCompositeKeyRecord { + private final String id; + private final String sort; + private final String pk1; + private final Integer pk2; + private final String pk3; + private final Instant pk4; + private final String sk1; + private final String sk2; + private final Instant sk3; + private final Integer sk4; + private final String data; + private final ImmutableFlattenedRecord flattenedRecord; + + private ImmutableCompositeKeyRecord(Builder builder) { + this.id = builder.id; + this.sort = builder.sort; + this.pk1 = builder.pk1; + this.pk2 = builder.pk2; + this.pk3 = builder.pk3; + this.pk4 = builder.pk4; + this.sk1 = builder.sk1; + this.sk2 = builder.sk2; + this.sk3 = builder.sk3; + this.sk4 = builder.sk4; + this.data = builder.data; + this.flattenedRecord = builder.flattenedRecord; + } + + public static Builder builder() { + return new Builder(); + } + + @DynamoDbPartitionKey + public String id() { + return this.id; + } + + @DynamoDbSortKey + public String sort() { + return this.sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"gsi1", "gsi2"}, order = Order.FIRST) + public String pk1() { + return this.pk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + @DynamoDbSecondaryPartitionKey(indexNames = "gsi3", order = Order.FIRST) + public Integer pk2() { + return this.pk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.THIRD) + public String pk3() { + return this.pk3; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi3", order = Order.SECOND) + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FOURTH) + public Instant pk4() { + return this.pk4; + } + + @DynamoDbSecondarySortKey(indexNames = {"gsi1", "gsi2"}, order = Order.FIRST) + public String sk1() { + return this.sk1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.SECOND) + public String sk2() { + return this.sk2; + } + + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.THIRD) + @DynamoDbSecondarySortKey(indexNames = "gsi3", order = Order.SECOND) + public Instant sk3() { + return this.sk3; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FOURTH) + public Integer sk4() { + return this.sk4; + } + + public String data() { + return this.data; + } + + @DynamoDbFlatten + public ImmutableFlattenedRecord flattenedRecord() { + return this.flattenedRecord; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ImmutableCompositeKeyRecord that = (ImmutableCompositeKeyRecord) obj; + return Objects.equals(id, that.id) && + Objects.equals(sort, that.sort) && + Objects.equals(pk1, that.pk1) && + Objects.equals(pk2, that.pk2) && + Objects.equals(pk3, that.pk3) && + Objects.equals(pk4, that.pk4) && + Objects.equals(sk1, that.sk1) && + Objects.equals(sk2, that.sk2) && + Objects.equals(sk3, that.sk3) && + Objects.equals(sk4, that.sk4) && + Objects.equals(data, that.data) && + Objects.equals(flattenedRecord, that.flattenedRecord); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, pk1, pk2, pk3, pk4, sk1, sk2, sk3, sk4, data, flattenedRecord); + } + + public static final class Builder { + private String id; + private String sort; + private String pk1; + private Integer pk2; + private String pk3; + private Instant pk4; + private String sk1; + private String sk2; + private Instant sk3; + private Integer sk4; + private String data; + private ImmutableFlattenedRecord flattenedRecord; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder sort(String sort) { + this.sort = sort; + return this; + } + + public Builder pk1(String pk1) { + this.pk1 = pk1; + return this; + } + + public Builder pk2(Integer pk2) { + this.pk2 = pk2; + return this; + } + + public Builder pk3(String pk3) { + this.pk3 = pk3; + return this; + } + + public Builder pk4(Instant pk4) { + this.pk4 = pk4; + return this; + } + + public Builder sk1(String sk1) { + this.sk1 = sk1; + return this; + } + + public Builder sk2(String sk2) { + this.sk2 = sk2; + return this; + } + + public Builder sk3(Instant sk3) { + this.sk3 = sk3; + return this; + } + + public Builder sk4(Integer sk4) { + this.sk4 = sk4; + return this; + } + + public Builder data(String data) { + this.data = data; + return this; + } + + public Builder flattenedRecord(ImmutableFlattenedRecord flattenedRecord) { + this.flattenedRecord = flattenedRecord; + return this; + } + + public ImmutableCompositeKeyRecord build() { + return new ImmutableCompositeKeyRecord(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/ImmutableFlattenedRecord.java b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/ImmutableFlattenedRecord.java new file mode 100644 index 000000000000..1306aaf76b7a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/it/java/software/amazon/awssdk/enhanced/dynamodb/model/ImmutableFlattenedRecord.java @@ -0,0 +1,128 @@ +/* + * 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.model; + +import java.time.Instant; +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbImmutable(builder = ImmutableFlattenedRecord.Builder.class) +public class ImmutableFlattenedRecord { + private final Double flpk2; + private final String flpk3; + private final String flsk2; + private final Instant flsk3; + private final String fldata; + + private ImmutableFlattenedRecord(Builder builder) { + this.flpk2 = builder.flpk2; + this.flpk3 = builder.flpk3; + this.flsk2 = builder.flsk2; + this.flsk3 = builder.flsk3; + this.fldata = builder.fldata; + } + + public static Builder builder() { + return new Builder(); + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi2", order = Order.SECOND) + public Double flpk2() { + return this.flpk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi2", order = Order.THIRD) + public String flpk3() { + return this.flpk3; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi2", order = Order.SECOND) + @DynamoDbSecondarySortKey(indexNames = "gsi3", order = Order.FIRST) + public String flsk2() { + return this.flsk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi2", order = Order.THIRD) + public Instant flsk3() { + return this.flsk3; + } + + public String fldata() { + return this.fldata; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ImmutableFlattenedRecord that = (ImmutableFlattenedRecord) obj; + return Objects.equals(flpk2, that.flpk2) && + Objects.equals(flpk3, that.flpk3) && + Objects.equals(flsk2, that.flsk2) && + Objects.equals(flsk3, that.flsk3) && + Objects.equals(fldata, that.fldata); + } + + @Override + public int hashCode() { + return Objects.hash(flpk2, flpk3, flsk2, flsk3, fldata); + } + + public static final class Builder { + private Double flpk2; + private String flpk3; + private String flsk2; + private Instant flsk3; + private String fldata; + + public Builder flpk2(Double flpk2) { + this.flpk2 = flpk2; + return this; + } + + public Builder flpk3(String flpk3) { + this.flpk3 = flpk3; + return this; + } + + public Builder flsk2(String flsk2) { + this.flsk2 = flsk2; + return this; + } + + public Builder flsk3(Instant flsk3) { + this.flsk3 = flsk3; + return this; + } + + public Builder fldata(String fldata) { + this.fldata = fldata; + return this; + } + + public ImmutableFlattenedRecord build() { + return new ImmutableFlattenedRecord(this); + } + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/ExecutionContext.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/ExecutionContext.java new file mode 100644 index 000000000000..558f701b0b8f --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/ExecutionContext.java @@ -0,0 +1,37 @@ +/* + * 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; + +import software.amazon.awssdk.annotations.SdkProtectedApi; + +/** + * Execution context used internally during table schema creation to track whether + * a schema is being created for a root entity or a flattened nested object. + */ +@SdkProtectedApi +public enum ExecutionContext { + /** + * Indicates schema creation for a top-level entity that will be directly used + * with DynamoDB operations. Enables schema caching for performance optimization. + */ + ROOT, + /** + * Indicates schema creation for a nested object marked with {@code @DynamoDbFlatten}. + * Bypasses caching to prevent conflicts and infinite recursion during flattened + * object processing. + */ + FLATTENED +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java index a92941e174c2..2093165a275f 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/IndexMetadata.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.enhanced.dynamodb; +import java.util.Collections; +import java.util.List; import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -30,13 +32,44 @@ public interface IndexMetadata { */ String name(); + /** + * The partition keys for the index in order. + * @default For backward compatibility the default implementation returns singleton list of partitionKey if present or empty + * list. + * External implementations of the interface must explicitly override this method to return the partitionKeys collection and + * to enable the composite key support to return multiple elements in the correct order. + */ + default List partitionKeys() { + return partitionKey().map(Collections::singletonList).orElse(Collections.emptyList()); + } + + /** + * The sort keys for the index in order. + * @default For backward compatibility the default implementation returns singleton list of sortKey if present or empty list. + * External implementations of the interface must explicitly override this method to return the sortKeys collection and + * to enable the composite key support to return multiple elements in the correct order. + */ + default List sortKeys() { + return sortKey().map(Collections::singletonList).orElse(Collections.emptyList()); + } + /** * The partition key for the index; if there is one. + * @deprecated Use {@link #partitionKeys()} for unified single/composite key support */ - Optional partitionKey(); + @Deprecated + default Optional partitionKey() { + List keys = partitionKeys(); + return keys.isEmpty() ? Optional.empty() : Optional.of(keys.get(0)); + } /** * The sort key for the index; if there is one. + * @deprecated Use {@link #sortKeys()} for unified single/composite key support */ - Optional sortKey(); + @Deprecated + default Optional sortKey() { + List keys = sortKeys(); + return keys.isEmpty() ? Optional.empty() : Optional.of(keys.get(0)); + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Key.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Key.java index 91ed7b1d924f..e8d18a156e11 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Key.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/Key.java @@ -15,11 +15,12 @@ package software.amazon.awssdk.enhanced.dynamodb; -import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; - +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -34,20 +35,30 @@ * 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. + * A key will always have partition key values associated with it, and optionally will have sort key values. + * Supports up to {@value #MAX_KEYS} partition keys and {@value #MAX_KEYS} sort keys. * The names of the keys themselves are not part of this object. */ @SdkPublicApi @ThreadSafe public final class Key { - private final AttributeValue partitionValue; - private final AttributeValue sortValue; + public static final int MAX_KEYS = 4; + + private final List partitionValues; + private final List sortValues; private Key(Builder builder) { - Validate.isTrue(builder.partitionValue != null && !builder.partitionValue.equals(nullAttributeValue()), - "partitionValue should not be null"); - this.partitionValue = builder.partitionValue; - this.sortValue = builder.sortValue; + if (builder.partitionValues == null || builder.partitionValues.isEmpty()) { + throw new IllegalArgumentException("partitionValues should not be null or empty"); + } + Validate.isTrue(builder.partitionValues.size() <= MAX_KEYS, + String.format("Maximum %s partition keys supported", MAX_KEYS)); + Validate.isTrue(builder.sortValues == null || builder.sortValues.size() <= MAX_KEYS, + String.format("Maximum %s sort keys supported", MAX_KEYS)); + + this.partitionValues = Collections.unmodifiableList(new ArrayList<>(builder.partitionValues)); + this.sortValues = builder.sortValues != null ? + Collections.unmodifiableList(new ArrayList<>(builder.sortValues)) : Collections.emptyList(); } /** @@ -60,43 +71,75 @@ public static Builder builder() { /** * Return a map of the key elements that can be passed directly to DynamoDb. - * @param tableSchema A tableschema to determine the key attribute names from. + * @param tableSchema A table schema to determine the key attribute names from. * @param index The name of the index to use when determining the key attribute names. * @return A map of attribute names to {@link AttributeValue}. */ public Map keyMap(TableSchema tableSchema, String index) { + Validate.notNull(tableSchema, "tableSchema must not be null"); Map keyMap = new HashMap<>(); - keyMap.put(tableSchema.tableMetadata().indexPartitionKey(index), partitionValue); - if (sortValue != null) { - keyMap.put(tableSchema.tableMetadata().indexSortKey(index).orElseThrow( - () -> new IllegalArgumentException("A sort key value was supplied for an index that does not support " - + "one. Index: " + index)), sortValue); + List partitionKeys = tableSchema.tableMetadata().indexPartitionKeys(index); + for (int i = 0; i < partitionKeys.size() && i < partitionValues.size(); i++) { + keyMap.put(partitionKeys.get(i), partitionValues.get(i)); + } + + if (!sortValues.isEmpty()) { + List sortKeys = tableSchema.tableMetadata().indexSortKeys(index); + if (sortKeys.isEmpty()) { + throw new IllegalArgumentException("Sort key values were supplied for an index that does not support " + + "sort keys. Index: " + index); + } + for (int i = 0; i < sortKeys.size() && i < sortValues.size(); i++) { + keyMap.put(sortKeys.get(i), sortValues.get(i)); + } } return Collections.unmodifiableMap(keyMap); } /** - * Get the literal value of the partition key stored in this object. - * @return An {@link AttributeValue} representing the literal value of the partition key. + * Get the literal values of all composite partition keys stored in this object. + * + * @return A list of {@link AttributeValue} representing the literal values of the partition keys. + */ + public List partitionKeyValues() { + return partitionValues; + } + + /** + * Get the literal values of all composite sort keys stored in this object. + * + * @return A list of {@link AttributeValue} representing the literal values of the sort keys. + */ + public List sortKeyValues() { + return sortValues; + } + + /** + * Get the literal value of the single partition key stored in this object. + *

+ * Use {@link #partitionKeyValues()} for composite key support + * @return An {@link AttributeValue} representing the literal value of the first partition key. */ public AttributeValue partitionKeyValue() { - return partitionValue; + return this.partitionValues.isEmpty() ? null : this.partitionValues.get(0); } /** - * Get the literal value of the sort key stored in this object if available. - * @return An optional {@link AttributeValue} representing the literal value of the sort key, or empty if there - * is no sort key value in this Key. + * Get the literal value of the single sort key stored in this object if available. + *

+ * Use {@link #sortKeyValues()} for composite key support + * @return An optional {@link AttributeValue} representing the literal value of the first sort key, or empty if there is no + * sort key value in this Key. */ public Optional sortKeyValue() { - return Optional.ofNullable(sortValue); + return Optional.ofNullable(this.sortValues.isEmpty() ? null : this.sortValues.get(0)); } /** * Return a map of the key elements that form the primary key of a table that can be passed directly to DynamoDb. - * @param tableSchema A tableschema to determine the key attribute names from. + * @param tableSchema A table schema to determine the key attribute names from. * @return A map of attribute names to {@link AttributeValue}. */ public Map primaryKeyMap(TableSchema tableSchema) { @@ -108,7 +151,7 @@ public Map primaryKeyMap(TableSchema tableSchema) { * @return A {@link Builder} initialized with the values of this key. */ public Builder toBuilder() { - return new Builder().partitionValue(this.partitionValue).sortValue(this.sortValue); + return new Builder().partitionValues(this.partitionValues).sortValues(this.sortValues); } /** @@ -116,81 +159,192 @@ public Builder toBuilder() { */ @NotThreadSafe public static final class Builder { - private AttributeValue partitionValue; - private AttributeValue sortValue; + + private static final DefaultAttributeConverterProvider DEFAULT_CONVERTER_PROVIDER = + DefaultAttributeConverterProvider.create(); + + private List partitionValues = new ArrayList<>(); + private List sortValues = new ArrayList<>(); private Builder() { } /** - * Value to be used for the partition key + * Values to be used for composite partition keys + * + * @param partitionValues list of partition key values (max {@value #MAX_KEYS}) + */ + public Builder partitionValues(List partitionValues) { + this.partitionValues = new ArrayList<>(partitionValues != null ? partitionValues : Collections.emptyList()); + return this; + } + + /** + * Values to be used for composite sort keys + * + * @param sortValues list of sort key values (max {@value #MAX_KEYS}) + */ + public Builder sortValues(List sortValues) { + this.sortValues = new ArrayList<>(sortValues != null ? sortValues : Collections.emptyList()); + return this; + } + + /** + * Adds a partition key value to this key builder for composite partition keys. + *

+ * The value will be automatically converted to a DynamoDB {@link AttributeValue} using the default + * attribute converter. Supported types include primitives, strings, numbers, binary data, and other + * standard Java types that can be mapped to DynamoDB attribute values. + *

+ * For composite partition keys, values are added in order (0, 1, 2, 3) and must match the order + * defined in the table schema. + * + * @param value the partition key value to add. Must not be null. + * @return this builder instance for method chaining + * @throws IllegalArgumentException if value is null or the value type cannot be converted to an AttributeValue + */ + public Builder addPartitionValue(Object value) { + if (value == null) { + throw new IllegalArgumentException("Partition key value cannot be null"); + } + partitionValues.add(convertToAttributeValue(value)); + return this; + } + + /** + * Adds a sort key value to this key builder for composite sort keys. + *

+ * The value will be automatically converted to a DynamoDB {@link AttributeValue} using the default + * attribute converter. Supported types include primitives, strings, numbers, binary data, and other + * standard Java types that can be mapped to DynamoDB attribute values. + *

+ * For composite sort keys, values are added in order (0, 1, 2, 3) and must match the order + * defined in the table schema. + * + * @param value the sort key value to add. Must not be null. + * @return this builder instance for method chaining + * @throws IllegalArgumentException if value is null or the value type cannot be converted to an AttributeValue + */ + public Builder addSortValue(Object value) { + if (value == null) { + throw new IllegalArgumentException("Sort key value cannot be null"); + } + sortValues.add(convertToAttributeValue(value)); + return this; + } + + @SuppressWarnings("unchecked") + private AttributeValue convertToAttributeValue(Object value) { + try { + EnhancedType type = (EnhancedType) EnhancedType.of(value.getClass()); + AttributeConverter converter = DEFAULT_CONVERTER_PROVIDER.converterFor(type); + return converter.transformFrom(value); + } catch (IllegalStateException e) { + throw new IllegalArgumentException("Unsupported type: " + value.getClass().getName(), e); + } + } + + /** + * Value to be used for the single partition key + *

+ * Use {@link #partitionValues(List)} or {@link #addPartitionValue(Object)} for composite key support * @param partitionValue partition key value */ public Builder partitionValue(AttributeValue partitionValue) { - this.partitionValue = partitionValue; + if (partitionValue == null || partitionValue.nul() != null && partitionValue.nul()) { + throw new IllegalArgumentException("partitionValue should not be null"); + } + this.partitionValues = Collections.singletonList(partitionValue); return this; } /** - * String value to be used for the partition key. The string will be converted into an AttributeValue of type S. + * String value to be used for the single partition key. The string will be converted into an AttributeValue of type S. + *

+ * Use {@link #partitionValues(List)} or {@link #addPartitionValue(Object)} for composite key support * @param partitionValue partition key value */ public Builder partitionValue(String partitionValue) { - this.partitionValue = AttributeValues.stringValue(partitionValue); + if (partitionValue == null) { + throw new IllegalArgumentException("partitionValue should not be null"); + } + this.partitionValues = Collections.singletonList(AttributeValues.stringValue(partitionValue)); return this; } /** - * Numeric value to be used for the partition key. The number will be converted into an AttributeValue of type N. + * Numeric value to be used for the single partition key. The number will be converted into an AttributeValue of type N. + *

+ * Use {@link #partitionValues(List)} or {@link #addPartitionValue(Object)} for composite key support * @param partitionValue partition key value */ public Builder partitionValue(Number partitionValue) { - this.partitionValue = AttributeValues.numberValue(partitionValue); + if (partitionValue == null) { + throw new IllegalArgumentException("partitionValue should not be null"); + } + this.partitionValues = Collections.singletonList(AttributeValues.numberValue(partitionValue)); return this; } /** - * Binary value to be used for the partition key. The input will be converted into an AttributeValue of type B. + * Binary value to be used for the single partition key. The input will be converted into an AttributeValue of type B. + *

+ * Use {@link #partitionValues(List)} or {@link #addPartitionValue(Object)} for composite key support * @param partitionValue the bytes to be used for the binary key value. */ public Builder partitionValue(SdkBytes partitionValue) { - this.partitionValue = AttributeValues.binaryValue(partitionValue); + if (partitionValue == null) { + throw new IllegalArgumentException("partitionValue should not be null"); + } + this.partitionValues = Collections.singletonList(AttributeValues.binaryValue(partitionValue)); return this; } /** - * Value to be used for the sort key + * Value to be used for the single sort key + *

+ * Use {@link #sortValues(List)} or {@link #addSortValue(Object)} for composite key support * @param sortValue sort key value */ public Builder sortValue(AttributeValue sortValue) { - this.sortValue = sortValue; + this.sortValues = sortValue != null ? + Collections.singletonList(sortValue) : Collections.emptyList(); return this; } /** - * String value to be used for the sort key. The string will be converted into an AttributeValue of type S. + * String value to be used for the single sort key. The string will be converted into an AttributeValue of type S. + *

+ * Use {@link #sortValues(List)} or {@link #addSortValue(Object)} for composite key support * @param sortValue sort key value */ public Builder sortValue(String sortValue) { - this.sortValue = AttributeValues.stringValue(sortValue); + this.sortValues = sortValue != null ? + Collections.singletonList(AttributeValues.stringValue(sortValue)) : Collections.emptyList(); return this; } /** - * Numeric value to be used for the sort key. The number will be converted into an AttributeValue of type N. + * Numeric value to be used for the single sort key. The number will be converted into an AttributeValue of type N. + *

+ * Use {@link #sortValues(List)} or {@link #addSortValue(Object)} for composite key support * @param sortValue sort key value */ public Builder sortValue(Number sortValue) { - this.sortValue = AttributeValues.numberValue(sortValue); + this.sortValues = sortValue != null ? + Collections.singletonList(AttributeValues.numberValue(sortValue)) : Collections.emptyList(); return this; } /** - * Binary value to be used for the sort key. The input will be converted into an AttributeValue of type B. + * Binary value to be used for the single sort key. The input will be converted into an AttributeValue of type B. + *

+ * Use {@link #sortValues(List)} or {@link #addSortValue(Object)} for composite key support * @param sortValue the bytes to be used for the binary key value. */ public Builder sortValue(SdkBytes sortValue) { - this.sortValue = AttributeValues.binaryValue(sortValue); + this.sortValues = sortValue != null ? + Collections.singletonList(AttributeValues.binaryValue(sortValue)) : Collections.emptyList(); return this; } @@ -213,17 +367,26 @@ public boolean equals(Object o) { Key key = (Key) o; - if (partitionValue != null ? ! partitionValue.equals(key.partitionValue) : - key.partitionValue != null) { - return false; - } - return sortValue != null ? sortValue.equals(key.sortValue) : key.sortValue == null; + return Objects.equals(partitionValues, key.partitionValues) && + Objects.equals(sortValues, key.sortValues); } @Override public int hashCode() { - int result = partitionValue != null ? partitionValue.hashCode() : 0; - result = 31 * result + (sortValue != null ? sortValue.hashCode() : 0); + int result = partitionValues == null || partitionValues.isEmpty() ? 0 + : listHashCode(partitionValues, 0); + + result = sortValues == null || sortValues.isEmpty() ? 31 * result + : listHashCode(sortValues, result); + + return result; + } + + private static int listHashCode(List list, int hash) { + int result = hash; + for (AttributeValue value : list) { + result = 31 * result + value.hashCode(); + } return result; } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java index b8680e5550da..559f70fd8a6e 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/KeyAttributeMetadata.java @@ -17,6 +17,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; /** * A metadata class that stores information about a key attribute @@ -33,4 +34,12 @@ public interface KeyAttributeMetadata { * The DynamoDB type of the key attribute */ AttributeValueType attributeValueType(); + + /** + * The order of the key attribute for composite keys. + * Default is -1 for implicit ordering. + */ + default Order order() { + return Order.UNSPECIFIED; + } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java index ee3ad5b46ba8..8cebb16ae68e 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadata.java @@ -16,6 +16,8 @@ package software.amazon.awssdk.enhanced.dynamodb; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -29,23 +31,63 @@ @ThreadSafe public interface TableMetadata { /** - * Returns the attribute name of the partition key for an index. + * Returns the attribute names of all the composite partition keys for an index. * * @param indexName The name of the index. - * @return The attribute name representing the partition key for this index. + * @return A list of attribute names representing the composite partition keys for this index. + * @throws IllegalArgumentException if the index does not exist in the metadata or does not have partition keys + * associated with it. + * @default For backward compatibility the default implementation returns singleton list of indexPartitionKey if present, or + * empty list. + * External implementations of the interface must explicitly override this method to return the indexPartitionKeys + * collection for composite key support. + */ + default List indexPartitionKeys(String indexName) { + String indexPartitionKey = indexPartitionKey(indexName); + return indexPartitionKey == null ? Collections.emptyList() : Collections.singletonList(indexPartitionKey); + } + + /** + * Returns the attribute name of the single partition key for an index. + *

+ * Use {@link #indexPartitionKeys(String)} for composite key support + * @param indexName The name of the index. + * @return The attribute name representing the first partition key for this index. * @throws IllegalArgumentException if the index does not exist in the metadata or does not have a partition key - * associated with it.. + * associated with it. */ - String indexPartitionKey(String indexName); + default String indexPartitionKey(String indexName) { + List keys = indexPartitionKeys(indexName); + return keys.isEmpty() ? null : keys.get(0); + } /** - * Returns the attribute name of the sort key for an index. + * Returns the attribute names of all the composite sort keys for an index. * * @param indexName The name of the index. - * @return Optional of the attribute name representing the sort key for this index; empty if the index does not + * @return A list of attribute names representing the composite sort keys for this index. + * @default For backward compatibility the default implementation returns singleton list of indexSortKey if present, or + * empty list. + * External implementations of the interface must explicitly override this method to return the indexSortKeys + * collection for composite key support. + */ + default List indexSortKeys(String indexName) { + Optional indexSortKey = indexSortKey(indexName); + return indexSortKey.map(Collections::singletonList).orElse(Collections.emptyList()); + } + + /** + * Returns the attribute name of the single sort key for an index. + *

+ * Use {@link #indexSortKeys(String)} for composite key support + * @param indexName The name of the index. + * @return Optional of the attribute name representing the first sort key for this index; empty if the index does not * have a sort key. */ - Optional indexSortKey(String indexName); + default Optional indexSortKey(String indexName) { + List keys = indexSortKeys(indexName); + return keys.isEmpty() ? Optional.empty() : Optional.of(keys.get(0)); + } /** * Returns a custom metadata object. These objects are used by extensions to the library, therefore the type of diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java index 068ea02ca919..9b673a14b0a2 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java @@ -29,6 +29,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchemaParams; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.TableSchemaFactory; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPreserveEmptyObject; @@ -200,16 +201,7 @@ static ImmutableTableSchema fromImmutableClass(ImmutableTableSchemaParams * @return An initialized {@link TableSchema} */ static TableSchema fromClass(Class annotatedClass) { - if (annotatedClass.getAnnotation(DynamoDbImmutable.class) != null) { - return fromImmutableClass(annotatedClass); - } - - if (annotatedClass.getAnnotation(DynamoDbBean.class) != null) { - return fromBean(annotatedClass); - } - - throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " + - "\"" + annotatedClass + "\"]"); + return TableSchemaFactory.fromClass(annotatedClass, ExecutionContext.ROOT); } /** diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BeginsWithConditional.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BeginsWithConditional.java index 04f297f4b2a5..3b386e36e2de 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BeginsWithConditional.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BeginsWithConditional.java @@ -15,9 +15,17 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.conditional; -import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.AND_OPERATOR; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.BEGINS_WITH_FUNCTION; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.FUNCTION_CLOSE; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.MISSING_SORT_VALUE_ERROR; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addNonRightmostSortKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addPartitionKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.buildExpression; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.resolveKeys; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validatePartitionKeyConstraints; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validateSortKeyConstraints; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -25,11 +33,14 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; +import software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.KeyResolution; import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @SdkInternalApi public class BeginsWithConditional implements QueryConditional { + private static final String BEGINS_WITH_NUMERIC_SORT_KEY_ERROR = + "Attempt to query using a 'beginsWith' condition operator against a numeric sort key. Index: %s, Attribute: %s"; private final Key key; @@ -39,40 +50,62 @@ public BeginsWithConditional(Key key) { @Override public Expression expression(TableSchema tableSchema, String indexName) { - QueryConditionalKeyValues queryConditionalKeyValues = QueryConditionalKeyValues.from(key, tableSchema, indexName); + KeyResolution keyResolution = resolveKeys(key, tableSchema, indexName); - if (queryConditionalKeyValues.sortValue().equals(nullAttributeValue())) { - throw new IllegalArgumentException("Attempt to query using a 'beginsWith' condition operator against a " - + "null sort key."); + validateBeginsWithConstraints(keyResolution, indexName); + + return buildBeginsWithExpression(keyResolution); + } + + private void validateBeginsWithConstraints(KeyResolution keyResolution, String indexName) { + validatePartitionKeyConstraints(keyResolution, indexName); + validateSortKeyConstraints(keyResolution, indexName); + + if (keyResolution.sortValues.isEmpty()) { + throw new IllegalArgumentException(String.format(MISSING_SORT_VALUE_ERROR, indexName)); } - if (queryConditionalKeyValues.sortValue().n() != null) { - throw new IllegalArgumentException("Attempt to query using a 'beginsWith' condition operator against " - + "a numeric sort key."); + AttributeValue rightmostSortValue = keyResolution.getRightmostSortValue(); + + if (rightmostSortValue.n() != null) { + throw new IllegalArgumentException(String.format(BEGINS_WITH_NUMERIC_SORT_KEY_ERROR, indexName, + keyResolution.getRightmostSortKey())); } + } + + private Expression buildBeginsWithExpression(KeyResolution keyResolution) { + StringBuilder expression = new StringBuilder(); + Map names = new HashMap<>(); + Map values = new HashMap<>(); + + addPartitionKeyConditions(expression, names, values, + keyResolution.partitionKeys, keyResolution.partitionValues); + + addNonRightmostSortKeyConditions(expression, names, values, + keyResolution.sortKeys, keyResolution.sortValues); + + addBeginsWithCondition(expression, names, values, keyResolution); + + return buildExpression(expression, names, values); + } + + private void addBeginsWithCondition(StringBuilder expression, Map names, + Map values, KeyResolution keyResolution) { + String rightmostSortKey = keyResolution.getRightmostSortKey(); + AttributeValue rightmostSortValue = keyResolution.getRightmostSortValue(); + + String keyToken = EnhancedClientUtils.keyRef(rightmostSortKey); + String valueToken = EnhancedClientUtils.valueRef(rightmostSortKey); + + expression.append(AND_OPERATOR) + .append(BEGINS_WITH_FUNCTION) + .append(keyToken) + .append(", ") + .append(valueToken) + .append(FUNCTION_CLOSE); - String partitionKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.partitionKey()); - String partitionValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.partitionKey()); - String sortKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.sortKey()); - String sortValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.sortKey()); - - String queryExpression = String.format("%s = %s AND begins_with ( %s, %s )", - partitionKeyToken, - partitionValueToken, - sortKeyToken, - sortValueToken); - Map expressionAttributeValues = new HashMap<>(); - expressionAttributeValues.put(partitionValueToken, queryConditionalKeyValues.partitionValue()); - expressionAttributeValues.put(sortValueToken, queryConditionalKeyValues.sortValue()); - Map expressionAttributeNames = new HashMap<>(); - expressionAttributeNames.put(partitionKeyToken, queryConditionalKeyValues.partitionKey()); - expressionAttributeNames.put(sortKeyToken, queryConditionalKeyValues.sortKey()); - - return Expression.builder() - .expression(queryExpression) - .expressionValues(Collections.unmodifiableMap(expressionAttributeValues)) - .expressionNames(expressionAttributeNames) - .build(); + names.put(keyToken, rightmostSortKey); + values.put(valueToken, rightmostSortValue); } @Override @@ -93,4 +126,4 @@ public boolean equals(Object o) { public int hashCode() { return key != null ? key.hashCode() : 0; } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditional.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditional.java index 3f4144fd5f2f..87a13222eb9c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditional.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/BetweenConditional.java @@ -16,25 +16,30 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.conditional; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; -import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.BETWEEN_OPERATOR; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.SECOND_VALUE_TOKEN_MAPPER; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addNonRightmostSortKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addPartitionKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.buildExpression; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.resolveKeys; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validatePartitionKeyConstraints; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validateSortKeyConstraints; -import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.function.UnaryOperator; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; +import software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.KeyResolution; import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @SdkInternalApi public class BetweenConditional implements QueryConditional { - - private static final UnaryOperator EXPRESSION_OTHER_VALUE_KEY_MAPPER = - k -> ":AMZN_MAPPED_" + cleanAttributeName(k) + "2"; + private static final String BETWEEN_NULL_SORT_KEY_ERROR = + "Attempt to query using a 'between' condition operator where one of the keys has a null sort key. Index: %s"; private final Key key1; private final Key key2; @@ -46,40 +51,61 @@ public BetweenConditional(Key key1, Key key2) { @Override public Expression expression(TableSchema tableSchema, String indexName) { - QueryConditionalKeyValues queryConditionalKeyValues1 = QueryConditionalKeyValues.from(key1, tableSchema, indexName); - QueryConditionalKeyValues queryConditionalKeyValues2 = QueryConditionalKeyValues.from(key2, tableSchema, indexName); + KeyResolution keyResolution1 = resolveKeys(key1, tableSchema, indexName); + KeyResolution keyResolution2 = resolveKeys(key2, tableSchema, indexName); + + validateBetweenConstraints(keyResolution1, keyResolution2, indexName); + + return buildBetweenExpression(keyResolution1, keyResolution2); + } + + private void validateBetweenConstraints(KeyResolution keyResolution1, KeyResolution keyResolution2, String indexName) { + validatePartitionKeyConstraints(keyResolution1, indexName); + validateSortKeyConstraints(keyResolution1, indexName); - if (queryConditionalKeyValues1.sortValue().equals(nullAttributeValue()) || - queryConditionalKeyValues2.sortValue().equals(nullAttributeValue())) { - throw new IllegalArgumentException("Attempt to query using a 'between' condition operator where one " - + "of the items has a null sort key."); + if (!keyResolution1.hasSortValues() || !keyResolution2.hasSortValues() || + keyResolution2.sortValues.contains(nullAttributeValue())) { + throw new IllegalArgumentException(String.format(BETWEEN_NULL_SORT_KEY_ERROR, indexName)); } + } + + private Expression buildBetweenExpression(KeyResolution keyResolution1, KeyResolution keyResolution2) { + StringBuilder expression = new StringBuilder(); + Map names = new HashMap<>(); + Map values = new HashMap<>(); + + addPartitionKeyConditions(expression, names, values, + keyResolution1.partitionKeys, keyResolution1.partitionValues); + + addNonRightmostSortKeyConditions(expression, names, values, + keyResolution1.sortKeys, keyResolution1.sortValues); + + addBetweenCondition(expression, names, values, keyResolution1, keyResolution2); + + return buildExpression(expression, names, values); + } - String partitionKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues1.partitionKey()); - String partitionValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues1.partitionKey()); - String sortKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues1.sortKey()); - String sortKeyValueToken1 = EnhancedClientUtils.valueRef(queryConditionalKeyValues1.sortKey()); - String sortKeyValueToken2 = EXPRESSION_OTHER_VALUE_KEY_MAPPER.apply(queryConditionalKeyValues2.sortKey()); - - String queryExpression = String.format("%s = %s AND %s BETWEEN %s AND %s", - partitionKeyToken, - partitionValueToken, - sortKeyToken, - sortKeyValueToken1, - sortKeyValueToken2); - Map expressionAttributeValues = new HashMap<>(); - expressionAttributeValues.put(partitionValueToken, queryConditionalKeyValues1.partitionValue()); - expressionAttributeValues.put(sortKeyValueToken1, queryConditionalKeyValues1.sortValue()); - expressionAttributeValues.put(sortKeyValueToken2, queryConditionalKeyValues2.sortValue()); - Map expressionAttributeNames = new HashMap<>(); - expressionAttributeNames.put(partitionKeyToken, queryConditionalKeyValues1.partitionKey()); - expressionAttributeNames.put(sortKeyToken, queryConditionalKeyValues1.sortKey()); - - return Expression.builder() - .expression(queryExpression) - .expressionValues(Collections.unmodifiableMap(expressionAttributeValues)) - .expressionNames(expressionAttributeNames) - .build(); + private void addBetweenCondition(StringBuilder expression, Map names, + Map values, + KeyResolution keyResolution1, KeyResolution keyResolution2) { + String rightmostSortKey = keyResolution1.getRightmostSortKey(); + AttributeValue rightmostSortValue1 = keyResolution1.getRightmostSortValue(); + AttributeValue rightmostSortValue2 = keyResolution2.getRightmostSortValue(); + + String keyToken = EnhancedClientUtils.keyRef(rightmostSortKey); + String valueToken1 = EnhancedClientUtils.valueRef(rightmostSortKey); + String valueToken2 = SECOND_VALUE_TOKEN_MAPPER.apply(rightmostSortKey); + + expression.append(QueryConditionalUtils.AND_OPERATOR) + .append(keyToken) + .append(BETWEEN_OPERATOR) + .append(valueToken1) + .append(QueryConditionalUtils.AND_OPERATOR) + .append(valueToken2); + + names.put(keyToken, rightmostSortKey); + values.put(valueToken1, rightmostSortValue1); + values.put(valueToken2, rightmostSortValue2); } @Override @@ -93,7 +119,7 @@ public boolean equals(Object o) { BetweenConditional that = (BetweenConditional) o; - if (key1 != null ? ! key1.equals(that.key1) : that.key1 != null) { + if (key1 != null ? !key1.equals(that.key1) : that.key1 != null) { return false; } return key2 != null ? key2.equals(that.key2) : that.key2 == null; @@ -105,4 +131,4 @@ public int hashCode() { result = 31 * result + (key2 != null ? key2.hashCode() : 0); return result; } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/EqualToConditional.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/EqualToConditional.java index 10bf765d0c75..93b88cfd8324 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/EqualToConditional.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/EqualToConditional.java @@ -15,18 +15,22 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.conditional; -import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; -import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.isNullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.AND_OPERATOR; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addEqualityCondition; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addPartitionKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.buildExpression; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.resolveKeys; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validatePartitionKeyConstraints; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validateSortKeyConstraints; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.Optional; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; +import software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.KeyResolution; import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -41,81 +45,39 @@ public EqualToConditional(Key key) { @Override public Expression expression(TableSchema tableSchema, String indexName) { - String partitionKey = tableSchema.tableMetadata().indexPartitionKey(indexName); - AttributeValue partitionValue = key.partitionKeyValue(); + KeyResolution keyResolution = resolveKeys(key, tableSchema, indexName); - if (partitionValue == null || partitionValue.equals(nullAttributeValue())) { - throw new IllegalArgumentException("Partition key must be a valid scalar value to execute a query " - + "against. The provided partition key was set to null."); - } - - Optional sortKeyValue = key.sortKeyValue(); + validateKeyConstraints(keyResolution, indexName); - if (sortKeyValue.isPresent()) { - Optional sortKey = tableSchema.tableMetadata().indexSortKey(indexName); - - if (!sortKey.isPresent()) { - throw new IllegalArgumentException("A sort key was supplied as part of a query conditional " - + "against an index that does not support a sort key. Index: " - + indexName); - } + return buildEqualityExpression(keyResolution); + } - return partitionAndSortExpression(partitionKey, - sortKey.get(), - partitionValue, - sortKeyValue.get()); - } else { - return partitionOnlyExpression(partitionKey, partitionValue); - } + private void validateKeyConstraints(KeyResolution keyResolution, String indexName) { + validatePartitionKeyConstraints(keyResolution, indexName); + validateSortKeyConstraints(keyResolution, indexName); } - private Expression partitionOnlyExpression(String partitionKey, - AttributeValue partitionValue) { + private Expression buildEqualityExpression(KeyResolution keyResolution) { + StringBuilder expression = new StringBuilder(); + Map names = new HashMap<>(); + Map values = new HashMap<>(); - String partitionKeyToken = EnhancedClientUtils.keyRef(partitionKey); - String partitionKeyValueToken = EnhancedClientUtils.valueRef(partitionKey); - String queryExpression = String.format("%s = %s", partitionKeyToken, partitionKeyValueToken); + addPartitionKeyConditions(expression, names, values, keyResolution.partitionKeys, keyResolution.partitionValues); - return Expression.builder() - .expression(queryExpression) - .expressionNames(Collections.singletonMap(partitionKeyToken, partitionKey)) - .expressionValues(Collections.singletonMap(partitionKeyValueToken, partitionValue)) - .build(); - } + addSortKeyEqualityConditions(expression, names, values, keyResolution); - private Expression partitionAndSortExpression(String partitionKey, - String sortKey, - AttributeValue partitionValue, - AttributeValue sortKeyValue) { + return buildExpression(expression, names, values); + } + private void addSortKeyEqualityConditions(StringBuilder expression, Map names, + Map values, KeyResolution keyResolution) { + List sortKeys = keyResolution.sortKeys; + List sortValues = keyResolution.sortValues; - // When a sort key is explicitly provided as null treat as partition only expression - if (isNullAttributeValue(sortKeyValue)) { - return partitionOnlyExpression(partitionKey, partitionValue); + for (int i = 0; i < sortValues.size(); i++) { + expression.append(AND_OPERATOR); + addEqualityCondition(expression, names, values, sortKeys.get(i), sortValues.get(i)); } - - String partitionKeyToken = EnhancedClientUtils.keyRef(partitionKey); - String partitionKeyValueToken = EnhancedClientUtils.valueRef(partitionKey); - String sortKeyToken = EnhancedClientUtils.keyRef(sortKey); - String sortKeyValueToken = EnhancedClientUtils.valueRef(sortKey); - - String queryExpression = String.format("%s = %s AND %s = %s", - partitionKeyToken, - partitionKeyValueToken, - sortKeyToken, - sortKeyValueToken); - Map expressionAttributeValues = new HashMap<>(); - expressionAttributeValues.put(partitionKeyValueToken, partitionValue); - expressionAttributeValues.put(sortKeyValueToken, sortKeyValue); - Map expressionAttributeNames = new HashMap<>(); - expressionAttributeNames.put(partitionKeyToken, partitionKey); - expressionAttributeNames.put(sortKeyToken, sortKey); - - return Expression.builder() - .expression(queryExpression) - .expressionValues(expressionAttributeValues) - .expressionNames(expressionAttributeNames) - .build(); } @Override @@ -136,4 +98,4 @@ public boolean equals(Object o) { public int hashCode() { return key != null ? key.hashCode() : 0; } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalKeyValues.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalKeyValues.java deleted file mode 100644 index 342eeaad15e7..000000000000 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalKeyValues.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.conditional; - -import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.enhanced.dynamodb.Key; -import software.amazon.awssdk.enhanced.dynamodb.TableSchema; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; - -/** - * Internal helper class to act as a struct to store specific key values that are used throughout various - * {@link software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional} implementations. - */ -@SdkInternalApi -class QueryConditionalKeyValues { - private final String partitionKey; - private final AttributeValue partitionValue; - private final String sortKey; - private final AttributeValue sortValue; - - private QueryConditionalKeyValues(String partitionKey, - AttributeValue partitionValue, - String sortKey, - AttributeValue sortValue) { - this.partitionKey = partitionKey; - this.partitionValue = partitionValue; - this.sortKey = sortKey; - this.sortValue = sortValue; - } - - static QueryConditionalKeyValues from(Key key, TableSchema tableSchema, String indexName) { - String partitionKey = tableSchema.tableMetadata().indexPartitionKey(indexName); - AttributeValue partitionValue = key.partitionKeyValue(); - String sortKey = tableSchema.tableMetadata().indexSortKey(indexName).orElseThrow( - () -> new IllegalArgumentException("A query conditional requires a sort key to be present on the table " - + "or index being queried, yet none have been defined in the " - + "model")); - AttributeValue sortValue = - key.sortKeyValue().orElseThrow( - () -> new IllegalArgumentException("A query conditional requires a sort key to compare with, " - + "however one was not provided.")); - - return new QueryConditionalKeyValues(partitionKey, partitionValue, sortKey, sortValue); - } - - String partitionKey() { - return partitionKey; - } - - AttributeValue partitionValue() { - return partitionValue; - } - - String sortKey() { - return sortKey; - } - - AttributeValue sortValue() { - return sortValue; - } -} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalUtils.java new file mode 100644 index 000000000000..fba34dd28385 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalUtils.java @@ -0,0 +1,237 @@ +/* + * 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.conditional; + +import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.cleanAttributeName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +@SdkInternalApi +public final class QueryConditionalUtils { + + public static final String AND_OPERATOR = " AND "; + public static final String EQUALITY_OPERATOR = " = "; + public static final String BETWEEN_OPERATOR = " BETWEEN "; + public static final String BEGINS_WITH_FUNCTION = "begins_with("; + public static final String FUNCTION_CLOSE = ")"; + + public static final String MISSING_SORT_VALUE_ERROR = + "A query conditional requires a sort key to compare with, however one was not provided. Index: %s"; + public static final UnaryOperator SECOND_VALUE_TOKEN_MAPPER = + k -> ":AMZN_MAPPED_" + cleanAttributeName(k) + "2"; + private static final String NULL_PARTITION_KEY_ERROR = + "Partition key must be a valid scalar value to execute a query against. The provided partition key was set to null. " + + "Index: %s"; + private static final String NULL_PARTITION_KEYS_ERROR = + "Partition key must be a valid scalar value to execute a query against. The provided composite partition keys contains " + + "null. Index: %s"; + private static final String PARTITION_KEY_SIZE_MISMATCH_ERROR = + "All partition key attributes must be provided for composite key query. Expected: %d, provided: %d. Index: %s"; + private static final String NULL_SORT_KEY_ERROR = "Attempt to query using a condition operator against a null sort key. " + + "Index: %s"; + private static final String SORT_KEY_SIZE_MISMATCH_ERROR = + "Cannot provide more sort key values than defined in schema. Index: %s"; + private static final String SORT_KEY_VALUES_WITHOUT_SCHEMA_ERROR = "Sort key values were supplied for an index that does" + + " not support sort keys. Index: %s"; + + private QueryConditionalUtils() { + } + + public static KeyResolution resolveKeys(Key key, TableSchema tableSchema, String indexName) { + List partitionKeys = resolvePartitionKeys(tableSchema, indexName); + List partitionValues = resolvePartitionValues(key); + + List sortKeys = resolveSortKeys(tableSchema, indexName); + List sortValues = resolveSortValues(key); + + return new KeyResolution(partitionKeys, partitionValues, sortKeys, sortValues); + } + + public static void addPartitionKeyConditions(StringBuilder expression, Map names, + Map values, + List partitionKeys, List partitionValues) { + for (int i = 0; i < partitionKeys.size(); i++) { + if (i > 0) { + expression.append(AND_OPERATOR); + } + addEqualityCondition(expression, names, values, partitionKeys.get(i), partitionValues.get(i)); + } + } + + public static void addNonRightmostSortKeyConditions(StringBuilder expression, Map names, + Map values, + List sortKeys, List sortValues) { + int rightmostIndex = sortValues.size() - 1; + for (int i = 0; i < rightmostIndex; i++) { + expression.append(AND_OPERATOR); + addEqualityCondition(expression, names, values, sortKeys.get(i), sortValues.get(i)); + } + } + + public static void addEqualityCondition(StringBuilder expression, Map names, + Map values, String key, AttributeValue value) { + String keyToken = EnhancedClientUtils.keyRef(key); + String valueToken = EnhancedClientUtils.valueRef(key); + + expression.append(keyToken).append(EQUALITY_OPERATOR).append(valueToken); + names.put(keyToken, key); + values.put(valueToken, value); + } + + public static Expression buildExpression(StringBuilder expression, Map names, + Map values) { + return Expression.builder() + .expression(expression.toString()) + .expressionNames(Collections.unmodifiableMap(names)) + .expressionValues(Collections.unmodifiableMap(values)) + .build(); + } + + public static void validatePartitionKeyConstraints(KeyResolution keyResolution, String indexName) { + if (keyResolution.isCompositePartition()) { + if (keyResolution.partitionValues.contains(nullAttributeValue())) { + throw new IllegalArgumentException(String.format(NULL_PARTITION_KEYS_ERROR, indexName)); + } + if (keyResolution.partitionValues.size() != keyResolution.partitionKeys.size()) { + throw new IllegalArgumentException(String.format(PARTITION_KEY_SIZE_MISMATCH_ERROR, + keyResolution.partitionKeys.size(), + keyResolution.partitionValues.size(), indexName)); + } + } else { + if (!keyResolution.hasPartitionValues() || keyResolution.partitionValues.get(0) == null || + keyResolution.partitionValues.get(0).equals(nullAttributeValue())) { + throw new IllegalArgumentException(String.format(NULL_PARTITION_KEY_ERROR, indexName)); + } + } + } + + public static void validateSortKeyConstraints(KeyResolution keyResolution, String indexName) { + if (keyResolution.hasSortKeys() || keyResolution.hasSortValues()) { + if (keyResolution.sortKeys.isEmpty()) { + throw new IllegalArgumentException(String.format(SORT_KEY_VALUES_WITHOUT_SCHEMA_ERROR, indexName)); + } + + if (keyResolution.sortValues.size() > keyResolution.sortKeys.size()) { + throw new IllegalArgumentException(String.format(SORT_KEY_SIZE_MISMATCH_ERROR, indexName)); + } + if (keyResolution.sortValues.contains(nullAttributeValue())) { + throw new IllegalArgumentException(String.format(NULL_SORT_KEY_ERROR, indexName)); + } + } + } + + private static List resolvePartitionKeys(TableSchema tableSchema, String indexName) { + List partitionKeys = tableSchema.tableMetadata().indexPartitionKeys(indexName); + if (!partitionKeys.isEmpty()) { + return partitionKeys; + } + + String partitionKey = tableSchema.tableMetadata().indexPartitionKey(indexName); + return Collections.singletonList(partitionKey); + } + + private static List resolvePartitionValues(Key key) { + List partitionValues = key.partitionKeyValues(); + if (!partitionValues.isEmpty()) { + return partitionValues; + } + + AttributeValue partitionValue = key.partitionKeyValue(); + return Collections.singletonList(partitionValue); + } + + private static List resolveSortKeys(TableSchema tableSchema, String indexName) { + List sortKeys = tableSchema.tableMetadata().indexSortKeys(indexName); + if (!sortKeys.isEmpty()) { + return sortKeys; + } + + return tableSchema.tableMetadata().indexSortKey(indexName) + .map(Collections::singletonList) + .orElse(Collections.emptyList()); + } + + private static List resolveSortValues(Key key) { + List sortValues = key.sortKeyValues(); + if (!sortValues.isEmpty()) { + return sortValues; + } + + return key.sortKeyValue() + .map(Collections::singletonList) + .orElse(Collections.emptyList()); + } + + public static class KeyResolution { + public final List partitionKeys; + public final List partitionValues; + public final List sortKeys; + public final List sortValues; + + public KeyResolution(List partitionKeys, List partitionValues, + List sortKeys, List sortValues) { + this.partitionKeys = Collections.unmodifiableList(new ArrayList<>(partitionKeys)); + this.partitionValues = Collections.unmodifiableList(new ArrayList<>(partitionValues)); + this.sortKeys = Collections.unmodifiableList(new ArrayList<>(sortKeys)); + this.sortValues = Collections.unmodifiableList(new ArrayList<>(sortValues)); + } + + public boolean hasPartitionValues() { + return !partitionValues.isEmpty(); + } + + public boolean hasSortKeys() { + return !sortKeys.isEmpty(); + } + + public boolean hasSortValues() { + return !sortValues.isEmpty(); + } + + public boolean isCompositePartition() { + return partitionKeys.size() > 1; + } + + public boolean isCompositeSort() { + return sortKeys.size() > 1; + } + + /** + * Returns the rightmost provided sort value. + */ + public AttributeValue getRightmostSortValue() { + return sortValues.get(sortValues.size() - 1); + } + + /** + * Returns the sort-key name corresponding to the rightmost provided sort value. + */ + public String getRightmostSortKey() { + return sortKeys.get(sortValues.size() - 1); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/SingleKeyItemConditional.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/SingleKeyItemConditional.java index 2d0a1a7d2a9c..7c91597d5734 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/SingleKeyItemConditional.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/SingleKeyItemConditional.java @@ -15,7 +15,13 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.conditional; -import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.MISSING_SORT_VALUE_ERROR; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addNonRightmostSortKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.addPartitionKeyConditions; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.buildExpression; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.resolveKeys; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validatePartitionKeyConstraints; +import static software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.validateSortKeyConstraints; import java.util.HashMap; import java.util.Map; @@ -24,6 +30,7 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils; +import software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.KeyResolution; import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -47,36 +54,54 @@ public SingleKeyItemConditional(Key key, String operator) { @Override public Expression expression(TableSchema tableSchema, String indexName) { - QueryConditionalKeyValues queryConditionalKeyValues = QueryConditionalKeyValues.from(key, tableSchema, indexName); + KeyResolution keyResolution = resolveKeys(key, tableSchema, indexName); - if (queryConditionalKeyValues.sortValue().equals(nullAttributeValue())) { - throw new IllegalArgumentException("Attempt to query using a relative condition operator against a " - + "null sort key."); + validateSingleKeyConstraints(keyResolution, indexName); + + return buildSingleKeyExpression(keyResolution); + } + + private void validateSingleKeyConstraints(KeyResolution keyResolution, String indexName) { + validatePartitionKeyConstraints(keyResolution, indexName); + validateSortKeyConstraints(keyResolution, indexName); + if (keyResolution.sortValues.isEmpty()) { + throw new IllegalArgumentException(String.format(MISSING_SORT_VALUE_ERROR, indexName)); } + } + + private Expression buildSingleKeyExpression(KeyResolution keyResolution) { + StringBuilder expression = new StringBuilder(); + Map names = new HashMap<>(); + Map values = new HashMap<>(); + + addPartitionKeyConditions(expression, names, values, + keyResolution.partitionKeys, keyResolution.partitionValues); + + addNonRightmostSortKeyConditions(expression, names, values, + keyResolution.sortKeys, keyResolution.sortValues); + + addOperatorCondition(expression, names, values, keyResolution); + + return buildExpression(expression, names, values); + } + + private void addOperatorCondition(StringBuilder expression, Map names, + Map values, KeyResolution keyResolution) { + String rightmostSortKey = keyResolution.getRightmostSortKey(); + AttributeValue rightmostSortValue = keyResolution.getRightmostSortValue(); + + String keyToken = EnhancedClientUtils.keyRef(rightmostSortKey); + String valueToken = EnhancedClientUtils.valueRef(rightmostSortKey); + + expression.append(QueryConditionalUtils.AND_OPERATOR) + .append(keyToken) + .append(" ") + .append(operator) + .append(" ") + .append(valueToken); - String partitionKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.partitionKey()); - String partitionValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.partitionKey()); - String sortKeyToken = EnhancedClientUtils.keyRef(queryConditionalKeyValues.sortKey()); - String sortValueToken = EnhancedClientUtils.valueRef(queryConditionalKeyValues.sortKey()); - - String queryExpression = String.format("%s = %s AND %s %s %s", - partitionKeyToken, - partitionValueToken, - sortKeyToken, - operator, - sortValueToken); - Map expressionAttributeValues = new HashMap<>(); - expressionAttributeValues.put(partitionValueToken, queryConditionalKeyValues.partitionValue()); - expressionAttributeValues.put(sortValueToken, queryConditionalKeyValues.sortValue()); - Map expressionAttributeNames = new HashMap<>(); - expressionAttributeNames.put(partitionKeyToken, queryConditionalKeyValues.partitionKey()); - expressionAttributeNames.put(sortKeyToken, queryConditionalKeyValues.sortKey()); - - return Expression.builder() - .expression(queryExpression) - .expressionValues(expressionAttributeValues) - .expressionNames(expressionAttributeNames) - .build(); + names.put(keyToken, rightmostSortKey); + values.put(valueToken, rightmostSortValue); } @Override @@ -90,7 +115,7 @@ public boolean equals(Object o) { SingleKeyItemConditional that = (SingleKeyItemConditional) o; - if (key != null ? ! key.equals(that.key) : that.key != null) { + if (key != null ? !key.equals(that.key) : that.key != null) { return false; } return operator != null ? operator.equals(that.operator) : that.operator == null; @@ -102,4 +127,4 @@ public int hashCode() { result = 31 * result + (operator != null ? operator.hashCode() : 0); return result; } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java index d56c4c13a1e0..3b0132ded2df 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanTableSchemaAttributeTags.java @@ -48,11 +48,11 @@ public static StaticAttributeTag attributeTagFor(DynamoDbSortKey annotation) { } public static StaticAttributeTag attributeTagFor(DynamoDbSecondaryPartitionKey annotation) { - return StaticAttributeTags.secondaryPartitionKey(Arrays.asList(annotation.indexNames())); + return StaticAttributeTags.secondaryPartitionKey(Arrays.asList(annotation.indexNames()), annotation.order()); } public static StaticAttributeTag attributeTagFor(DynamoDbSecondarySortKey annotation) { - return StaticAttributeTags.secondarySortKey(Arrays.asList(annotation.indexNames())); + return StaticAttributeTags.secondarySortKey(Arrays.asList(annotation.indexNames()), annotation.order()); } public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotation) { @@ -62,4 +62,4 @@ public static StaticAttributeTag attributeTagFor(DynamoDbUpdateBehavior annotati public static StaticAttributeTag attributeTagFor(DynamoDbAtomicCounter annotation) { return StaticAttributeTags.atomicCounter(annotation.delta(), annotation.startValue()); } -} +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java index 4d253b82ba57..f56bf67a9534 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticIndexMetadata.java @@ -15,7 +15,12 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.mapper; -import java.util.Optional; +import static java.util.Comparator.comparingInt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; @@ -24,13 +29,21 @@ @SdkInternalApi public class StaticIndexMetadata implements IndexMetadata { private final String name; - private final KeyAttributeMetadata partitionKey; - private final KeyAttributeMetadata sortKey; + private final List partitionKeys; + private final List sortKeys; private StaticIndexMetadata(Builder b) { this.name = b.name; - this.partitionKey = b.partitionKey; - this.sortKey = b.sortKey; + this.partitionKeys = Collections.unmodifiableList( + b.partitionKeys.stream() + .sorted(comparingInt(key -> key.order().getIndex())) + .collect(Collectors.toList()) + ); + this.sortKeys = Collections.unmodifiableList( + b.sortKeys.stream() + .sorted(comparingInt(key -> key.order().getIndex())) + .collect(Collectors.toList()) + ); } public static Builder builder() { @@ -38,9 +51,12 @@ public static Builder builder() { } public static Builder builderFrom(IndexMetadata index) { - return index == null ? builder() : builder().name(index.name()) - .partitionKey(index.partitionKey().orElse(null)) - .sortKey(index.sortKey().orElse(null)); + if (index == null) { + return builder(); + } + return builder().name(index.name()) + .partitionKeys(index.partitionKeys()) + .sortKeys(index.sortKeys()); } @Override @@ -49,20 +65,20 @@ public String name() { } @Override - public Optional partitionKey() { - return Optional.ofNullable(this.partitionKey); + public List partitionKeys() { + return this.partitionKeys; } @Override - public Optional sortKey() { - return Optional.ofNullable(this.sortKey); + public List sortKeys() { + return this.sortKeys; } @NotThreadSafe public static class Builder { private String name; - private KeyAttributeMetadata partitionKey; - private KeyAttributeMetadata sortKey; + private List partitionKeys = new ArrayList<>(); + private List sortKeys = new ArrayList<>(); private Builder() { } @@ -72,16 +88,52 @@ public Builder name(String name) { return this; } + public Builder partitionKeys(List partitionKeys) { + this.partitionKeys = new ArrayList<>(partitionKeys); + return this; + } + + public Builder sortKeys(List sortKeys) { + this.sortKeys = new ArrayList<>(sortKeys); + return this; + } + + public Builder addPartitionKey(KeyAttributeMetadata partitionKey) { + this.partitionKeys.add(partitionKey); + return this; + } + + public Builder addSortKey(KeyAttributeMetadata sortKey) { + this.sortKeys.add(sortKey); + return this; + } + + public List getPartitionKeys() { + return new ArrayList<>(this.partitionKeys); + } + + public List getSortKeys() { + return new ArrayList<>(this.sortKeys); + } + + // Backward compatibility methods public Builder partitionKey(KeyAttributeMetadata partitionKey) { - this.partitionKey = partitionKey; + this.partitionKeys = new ArrayList<>(); + if (partitionKey != null) { + this.partitionKeys.add(partitionKey); + } return this; } public Builder sortKey(KeyAttributeMetadata sortKey) { - this.sortKey = sortKey; + this.sortKeys = new ArrayList<>(); + if (sortKey != null) { + this.sortKeys.add(sortKey); + } return this; } + public StaticIndexMetadata build() { return new StaticIndexMetadata(this); } @@ -101,17 +153,25 @@ public boolean equals(Object o) { if (name != null ? !name.equals(that.name) : that.name != null) { return false; } - if (partitionKey != null ? !partitionKey.equals(that.partitionKey) : that.partitionKey != null) { + if (!partitionKeys.equals(that.partitionKeys)) { return false; } - return sortKey != null ? sortKey.equals(that.sortKey) : that.sortKey == null; + return sortKeys.equals(that.sortKeys); } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; - result = 31 * result + (partitionKey != null ? partitionKey.hashCode() : 0); - result = 31 * result + (sortKey != null ? sortKey.hashCode() : 0); + result = listHashCode(partitionKeys, result); + result = listHashCode(sortKeys, result); + return result; + } + + private static int listHashCode(List list, int hash) { + int result = hash; + for (KeyAttributeMetadata key : list) { + result = 31 * result + key.hashCode(); + } return result; } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java index 05af635cbce0..4885a39b899b 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticKeyAttributeMetadata.java @@ -18,19 +18,26 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; @SdkInternalApi public class StaticKeyAttributeMetadata implements KeyAttributeMetadata { private final String name; private final AttributeValueType attributeValueType; + private final Order order; - private StaticKeyAttributeMetadata(String name, AttributeValueType attributeValueType) { + private StaticKeyAttributeMetadata(String name, AttributeValueType attributeValueType, Order order) { this.name = name; this.attributeValueType = attributeValueType; + this.order = order; } public static StaticKeyAttributeMetadata create(String name, AttributeValueType attributeValueType) { - return new StaticKeyAttributeMetadata(name, attributeValueType); + return new StaticKeyAttributeMetadata(name, attributeValueType, Order.UNSPECIFIED); + } + + public static StaticKeyAttributeMetadata create(String name, AttributeValueType attributeValueType, Order order) { + return new StaticKeyAttributeMetadata(name, attributeValueType, order); } @Override @@ -43,6 +50,11 @@ public AttributeValueType attributeValueType() { return this.attributeValueType; } + @Override + public Order order() { + return this.order; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -57,13 +69,17 @@ public boolean equals(Object o) { if (name != null ? !name.equals(staticKey.name) : staticKey.name != null) { return false; } - return attributeValueType == staticKey.attributeValueType; + if (attributeValueType != staticKey.attributeValueType) { + return false; + } + return order == staticKey.order; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (attributeValueType != null ? attributeValueType.hashCode() : 0); + result = 31 * result + order.hashCode(); return result; } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperation.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperation.java index 055b685dbe4d..487d4b7c7b15 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperation.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperation.java @@ -15,12 +15,11 @@ package software.amazon.awssdk.enhanced.dynamodb.internal.operations; -import java.util.Arrays; + import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Function; @@ -37,8 +36,10 @@ import software.amazon.awssdk.services.dynamodb.model.BillingMode; 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.KeySchemaElement; import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex; @SdkInternalApi public class CreateTableOperation implements TableOperation { @@ -63,78 +64,31 @@ public CreateTableRequest generateRequest(TableSchema tableSchema, OperationContext operationContext, DynamoDbEnhancedClientExtension extension) { if (!TableMetadata.primaryIndexName().equals(operationContext.indexName())) { - throw new IllegalArgumentException("PutItem cannot be executed against a secondary index."); + throw new IllegalArgumentException("CreateTable cannot be executed against a secondary index."); } - String primaryPartitionKey = tableSchema.tableMetadata().primaryPartitionKey(); - Optional primarySortKey = tableSchema.tableMetadata().primarySortKey(); - Set dedupedIndexKeys = new HashSet<>(); - dedupedIndexKeys.add(primaryPartitionKey); - primarySortKey.ifPresent(dedupedIndexKeys::add); - List sdkGlobalSecondaryIndices = null; - List sdkLocalSecondaryIndices = null; - - if (this.request.globalSecondaryIndices() != null && !this.request.globalSecondaryIndices().isEmpty()) { - sdkGlobalSecondaryIndices = - this.request.globalSecondaryIndices().stream().map(gsi -> { - String indexPartitionKey = tableSchema.tableMetadata().indexPartitionKey(gsi.indexName()); - Optional indexSortKey = tableSchema.tableMetadata().indexSortKey(gsi.indexName()); - dedupedIndexKeys.add(indexPartitionKey); - indexSortKey.ifPresent(dedupedIndexKeys::add); - - return software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex - .builder() - .indexName(gsi.indexName()) - .keySchema(generateKeySchema(indexPartitionKey, indexSortKey.orElse(null))) - .projection(gsi.projection()) - .provisionedThroughput(gsi.provisionedThroughput()) - .build(); - }).collect(Collectors.toList()); - } + List primaryPartitionKeys = tableSchema.tableMetadata().indexPartitionKeys(TableMetadata.primaryIndexName()); + List primarySortKeys = tableSchema.tableMetadata().indexSortKeys(TableMetadata.primaryIndexName()); - if (this.request.localSecondaryIndices() != null && !this.request.localSecondaryIndices().isEmpty()) { - sdkLocalSecondaryIndices = - this.request.localSecondaryIndices().stream().map(lsi -> { - Optional indexSortKey = tableSchema.tableMetadata().indexSortKey(lsi.indexName()); - indexSortKey.ifPresent(dedupedIndexKeys::add); - - if (!primaryPartitionKey.equals( - tableSchema.tableMetadata().indexPartitionKey(lsi.indexName()))) { - throw new IllegalArgumentException("Attempt to create a local secondary index with a partition " - + "key that is not the primary partition key. Index name: " - + lsi.indexName()); - } - - return software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex - .builder() - .indexName(lsi.indexName()) - .keySchema(generateKeySchema(primaryPartitionKey, indexSortKey.orElse(null))) - .projection(lsi.projection()) - .build(); - }).collect(Collectors.toList()); - } + validatePrimaryKeys(primaryPartitionKeys, primarySortKeys); - List attributeDefinitions = - dedupedIndexKeys.stream() - .map(attribute -> - AttributeDefinition.builder() - .attributeName(attribute) - .attributeType(tableSchema - .tableMetadata().scalarAttributeType(attribute) - .orElseThrow(() -> - new IllegalArgumentException( - "Could not map the key attribute '" + attribute + - "' to a valid scalar type."))) - .build()) - .collect(Collectors.toList()); + Set dedupedIndexKeys = new HashSet<>(); + dedupedIndexKeys.addAll(primaryPartitionKeys); + dedupedIndexKeys.addAll(primarySortKeys); + + List sdkGlobalSecondaryIndices = buildGlobalSecondaryIndices(tableSchema, dedupedIndexKeys); + List sdkLocalSecondaryIndices = buildLocalSecondaryIndices(tableSchema, dedupedIndexKeys, + primaryPartitionKeys); + List attributeDefinitions = buildAttributeDefinitions(dedupedIndexKeys, tableSchema); + BillingMode billingMode = this.request.provisionedThroughput() == null ? BillingMode.PAY_PER_REQUEST : BillingMode.PROVISIONED; return CreateTableRequest.builder() .tableName(operationContext.tableName()) - .keySchema(generateKeySchema(primaryPartitionKey, primarySortKey.orElse(null))) + .keySchema(generateKeySchema(primaryPartitionKeys, primarySortKeys)) .globalSecondaryIndexes(sdkGlobalSecondaryIndices) .localSecondaryIndexes(sdkLocalSecondaryIndices) .attributeDefinitions(attributeDefinitions) @@ -165,26 +119,111 @@ public Void transformResponse(CreateTableResponse response, return null; } - private static Collection generateKeySchema(String partitionKey, String sortKey) { - if (sortKey == null) { - return generateKeySchema(partitionKey); + private void validatePrimaryKeys(List primaryPartitionKeys, List primarySortKeys) { + if (primaryPartitionKeys.isEmpty()) { + throw new IllegalArgumentException("Primary partition key is required for table creation"); + } + if (primaryPartitionKeys.size() > 1) { + throw new IllegalArgumentException("Primary table does not support composite partition keys"); + } + if (primarySortKeys.size() > 1) { + throw new IllegalArgumentException("Primary table does not support composite sort keys"); + } + } + + private List buildGlobalSecondaryIndices(TableSchema tableSchema, Set dedupedIndexKeys) { + if (!hasIndices(this.request.globalSecondaryIndices())) { + return null; + } + + return this.request.globalSecondaryIndices().stream().map(gsi -> { + List indexPartitionKeys = tableSchema.tableMetadata().indexPartitionKeys(gsi.indexName()); + List indexSortKeys = tableSchema.tableMetadata().indexSortKeys(gsi.indexName()); + dedupedIndexKeys.addAll(indexPartitionKeys); + dedupedIndexKeys.addAll(indexSortKeys); + + return GlobalSecondaryIndex.builder() + .indexName(gsi.indexName()) + .keySchema(generateKeySchema(indexPartitionKeys, indexSortKeys)) + .projection(gsi.projection()) + .provisionedThroughput(gsi.provisionedThroughput()) + .build(); + }).collect(Collectors.toList()); + } + + private List buildLocalSecondaryIndices( + TableSchema tableSchema, Set dedupedIndexKeys, List primaryPartitionKeys) { + if (!hasIndices(this.request.localSecondaryIndices())) { + return null; } - return Collections.unmodifiableList(Arrays.asList(KeySchemaElement.builder() - .attributeName(partitionKey) - .keyType(KeyType.HASH) - .build(), - KeySchemaElement.builder() - .attributeName(sortKey) - .keyType(KeyType.RANGE) - .build())); + return this.request.localSecondaryIndices().stream().map(lsi -> { + List lsiPartitionKeys = tableSchema.tableMetadata().indexPartitionKeys(lsi.indexName()); + List lsiSortKeys = tableSchema.tableMetadata().indexSortKeys(lsi.indexName()); + + validateLsiConstraints(primaryPartitionKeys, lsiPartitionKeys, lsiSortKeys, lsi.indexName()); + + dedupedIndexKeys.addAll(lsiPartitionKeys); + dedupedIndexKeys.addAll(lsiSortKeys); + + return LocalSecondaryIndex.builder() + .indexName(lsi.indexName()) + .keySchema(generateKeySchema(lsiPartitionKeys, lsiSortKeys)) + .projection(lsi.projection()) + .build(); + }).collect(Collectors.toList()); + } + + private void validateLsiConstraints(List primaryPartitionKeys, List lsiPartitionKeys, + List lsiSortKeys, String indexName) { + if (lsiPartitionKeys.size() != 1) { + throw new IllegalArgumentException("LSI must have exactly one partition key. Index: " + indexName); + } + + if (!primaryPartitionKeys.get(0).equals(lsiPartitionKeys.get(0))) { + throw new IllegalArgumentException("LSI partition key must match primary partition key. Index: " + indexName); + } + + if (lsiSortKeys.size() > 1) { + throw new IllegalArgumentException("LSI does not support composite sort keys. Index: " + indexName); + } + } + + private List buildAttributeDefinitions(Set dedupedIndexKeys, + TableSchema tableSchema) { + return dedupedIndexKeys.stream() + .map(attribute -> AttributeDefinition.builder() + .attributeName(attribute) + .attributeType(tableSchema.tableMetadata() + .scalarAttributeType(attribute) + .orElseThrow(() -> + new IllegalArgumentException( + String.format( + "Could not map key attribute '%s' to a valid scalar type", + attribute)))) + .build()) + .collect(Collectors.toList()); + } + + private boolean hasIndices(Collection indices) { + return indices != null && !indices.isEmpty(); } - private static Collection generateKeySchema(String partitionKey) { - return Collections.singletonList(KeySchemaElement.builder() - .attributeName(partitionKey) - .keyType(KeyType.HASH) - .build()); + private static Collection generateKeySchema(Collection partitionKeys, + Collection sortKeys) { + List keySchema = partitionKeys.stream() + .map(partitionKey -> KeySchemaElement.builder() + .attributeName(partitionKey) + .keyType(KeyType.HASH) + .build()) + .collect(Collectors.toList()); + + sortKeys.stream().map(sortKey -> KeySchemaElement.builder() + .attributeName(sortKey) + .keyType(KeyType.RANGE) + .build()).forEach(keySchema::add); + + return Collections.unmodifiableList(keySchema); } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/AnnotationUtils.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/AnnotationUtils.java new file mode 100644 index 000000000000..830cb24fdffe --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/AnnotationUtils.java @@ -0,0 +1,78 @@ +/* + * 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.mapper; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.awssdk.annotations.SdkInternalApi; + +@SdkInternalApi +public final class AnnotationUtils { + + private AnnotationUtils() { + } + + /** + * Expands annotation arrays to handle repeatable annotations by extracting individual + * annotations from container annotations. + */ + public static List expandAnnotations(Annotation[]... annotationArrays) { + if (annotationArrays == null || annotationArrays.length == 0) { + return Collections.emptyList(); + } + + return Arrays.stream(annotationArrays) + .filter(annotations -> annotations != null && annotations.length > 0) + .flatMap(Arrays::stream) + .flatMap(AnnotationUtils::expandSingleAnnotation) + .collect(Collectors.toList()); + } + + private static Stream expandSingleAnnotation(Annotation annotation) { + if (annotation == null) { + return Stream.empty(); + } + + List containerAnnotations = extractFromContainer(annotation); + return containerAnnotations.isEmpty() + ? Stream.of(annotation) + : containerAnnotations.stream(); + } + + private static List extractFromContainer(Annotation annotation) { + try { + Method valueMethod = annotation.annotationType().getDeclaredMethod("value"); + Class returnType = valueMethod.getReturnType(); + + if (!returnType.isArray() || !returnType.getComponentType().isAnnotation()) { + return Collections.emptyList(); + } + + Annotation[] containedAnnotations = (Annotation[]) valueMethod.invoke(annotation); + return containedAnnotations != null ? Arrays.asList(containedAnnotations) : Collections.emptyList(); + + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | + SecurityException | ClassCastException e) { + return Collections.emptyList(); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java index b365419194be..90f8231cd26c 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchema.java @@ -42,7 +42,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -51,6 +50,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.EnhancedTypeDocumentConfiguration; +import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeConfiguration; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanAttributeGetter; @@ -140,8 +140,12 @@ private BeanTableSchema(StaticTableSchema staticTableSchema) { */ @SuppressWarnings("unchecked") public static BeanTableSchema create(Class beanClass) { + return create(beanClass, ExecutionContext.ROOT); + } + + static BeanTableSchema create(Class beanClass, ExecutionContext context) { BeanTableSchemaParams params = BeanTableSchemaParams.builder(beanClass).build(); - return create(params); + return create(params, context); } /** @@ -162,19 +166,28 @@ public static BeanTableSchema create(Class beanClass) { */ @SuppressWarnings("unchecked") public static BeanTableSchema create(BeanTableSchemaParams params) { - return (BeanTableSchema) BEAN_TABLE_SCHEMA_CACHE.computeIfAbsent(params.beanClass(), - clz -> create(params, - new MetaTableSchemaCache())); + return create(params, ExecutionContext.ROOT); } - private static BeanTableSchema create(BeanTableSchemaParams params, MetaTableSchemaCache metaTableSchemaCache) { + private static BeanTableSchema create(BeanTableSchemaParams params, ExecutionContext context) { + if (context == ExecutionContext.ROOT) { + return (BeanTableSchema) BEAN_TABLE_SCHEMA_CACHE.computeIfAbsent(params.beanClass(), + clz -> create(params, + new MetaTableSchemaCache(), + context)); + } + return create(params, new MetaTableSchemaCache(), context); + } + + private static BeanTableSchema create(BeanTableSchemaParams params, MetaTableSchemaCache metaTableSchemaCache, + ExecutionContext context) { Class beanClass = params.beanClass(); debugLog(beanClass, () -> "Creating bean schema"); // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache MetaTableSchema metaTableSchema = metaTableSchemaCache.getOrCreate(beanClass); BeanTableSchema newTableSchema = - new BeanTableSchema<>(createStaticTableSchema(params.beanClass(), params.lookup(), metaTableSchemaCache)); + new BeanTableSchema<>(createStaticTableSchema(params.beanClass(), params.lookup(), metaTableSchemaCache, context)); metaTableSchema.initialize(newTableSchema); return newTableSchema; } @@ -204,7 +217,8 @@ static TableSchema recursiveCreate(Class beanClass, MethodHandles.Look private static StaticTableSchema createStaticTableSchema(Class beanClass, MethodHandles.Lookup lookup, - MetaTableSchemaCache metaTableSchemaCache) { + MetaTableSchemaCache metaTableSchemaCache, + ExecutionContext context) { DynamoDbBean dynamoDbBean = beanClass.getAnnotation(DynamoDbBean.class); @@ -249,7 +263,8 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla setterForProperty(propertyDescriptor, beanClass, lookup)); } else { // Object flattening - builder.flatten(TableSchema.fromClass(propertyDescriptor.getReadMethod().getReturnType()), + builder.flatten(TableSchemaFactory.fromClass(propertyDescriptor.getReadMethod().getReturnType(), + ExecutionContext.FLATTENED), getterForProperty(propertyDescriptor, beanClass, lookup), setterForProperty(propertyDescriptor, beanClass, lookup)); } @@ -272,7 +287,7 @@ private static StaticTableSchema createStaticTableSchema(Class beanCla builder.attributes(attributes); - return builder.build(); + return builder.build(context); } // Enhance beanInfo descriptors with fluent setter when the default set method is absent @@ -301,12 +316,12 @@ private static Optional findFluentSetter(Class beanClass, String prop private static void validateDynamoDbFlattenAnnotations(List mappableProperties) { int mapCount = 0; - + for (PropertyDescriptor property : mappableProperties) { if (!hasFlattenAnnotation(property)) { continue; } - + Type type = property.getReadMethod().getGenericReturnType(); if (isValidFlattenMapType(type)) { mapCount++; @@ -315,27 +330,27 @@ private static void validateDynamoDbFlattenAnnotations(List "@DynamoDbFlatten on Map properties can only be applied to Map attributes"); } } - + if (mapCount > 1) { throw new IllegalArgumentException("Multiple @DynamoDbFlatten Map properties found. " + "Only one flattened map per class is supported."); } } - + private static boolean hasFlattenAnnotation(PropertyDescriptor property) { return getPropertyAnnotation(property, DynamoDbFlatten.class) != null; } - + private static boolean isMapType(Type type) { - return type instanceof ParameterizedType && + return type instanceof ParameterizedType && Map.class.equals(((ParameterizedType) type).getRawType()); } - + private static boolean isValidFlattenMapType(Type type) { if (!isMapType(type)) { return false; } - + Type[] mapTypes = ((ParameterizedType) type).getActualTypeArguments(); return mapTypes.length == 2 && @@ -575,9 +590,8 @@ private static R getPropertyAnnotation(PropertyDescriptor } private static List propertyAnnotations(PropertyDescriptor propertyDescriptor) { - return Stream.concat(Arrays.stream(propertyDescriptor.getReadMethod().getAnnotations()), - Arrays.stream(propertyDescriptor.getWriteMethod().getAnnotations())) - .collect(Collectors.toList()); + return AnnotationUtils.expandAnnotations(propertyDescriptor.getReadMethod().getAnnotations(), + propertyDescriptor.getWriteMethod().getAnnotations()); } private static void debugLog(Class beanClass, Supplier logMessage) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java index 14ae05c0f5db..853151bfbde3 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchema.java @@ -37,7 +37,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -46,6 +45,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; import software.amazon.awssdk.enhanced.dynamodb.EnhancedTypeDocumentConfiguration; +import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeConfiguration; import software.amazon.awssdk.enhanced.dynamodb.internal.immutable.ImmutableInfo; @@ -136,8 +136,15 @@ private ImmutableTableSchema(StaticImmutableTableSchema wrappedTableSchema */ @SuppressWarnings("unchecked") public static ImmutableTableSchema create(ImmutableTableSchemaParams params) { - return (ImmutableTableSchema) IMMUTABLE_TABLE_SCHEMA_CACHE.computeIfAbsent( - params.immutableClass(), clz -> create(params, new MetaTableSchemaCache())); + return create(params, ExecutionContext.ROOT); + } + + private static ImmutableTableSchema create(ImmutableTableSchemaParams params, ExecutionContext context) { + if (context == ExecutionContext.ROOT) { + return (ImmutableTableSchema) IMMUTABLE_TABLE_SCHEMA_CACHE.computeIfAbsent( + params.immutableClass(), clz -> create(params, new MetaTableSchemaCache(), context)); + } + return create(params, new MetaTableSchemaCache(), context); } /** @@ -158,11 +165,16 @@ public static ImmutableTableSchema create(ImmutableTableSchemaParams p */ @SuppressWarnings("unchecked") public static ImmutableTableSchema create(Class immutableClass) { - return create(ImmutableTableSchemaParams.builder(immutableClass).build()); + return create(immutableClass, ExecutionContext.ROOT); + } + + static ImmutableTableSchema create(Class immutableClass, ExecutionContext context) { + return create(ImmutableTableSchemaParams.builder(immutableClass).build(), context); } private static ImmutableTableSchema create(ImmutableTableSchemaParams params, - MetaTableSchemaCache metaTableSchemaCache) { + MetaTableSchemaCache metaTableSchemaCache, + ExecutionContext context) { debugLog(params.immutableClass(), () -> "Creating immutable schema"); // Fetch or create a new reference to this yet-to-be-created TableSchema in the cache @@ -172,6 +184,11 @@ private static ImmutableTableSchema create(ImmutableTableSchemaParams new ImmutableTableSchema<>(createStaticImmutableTableSchema(params.immutableClass(), params.lookup(), metaTableSchemaCache)); + + if (context == ExecutionContext.ROOT) { + IndexValidator.validateAllIndices(newTableSchema.delegateTableSchema().tableMetadata().indices()); + } + metaTableSchema.initialize(newTableSchema); return newTableSchema; } @@ -195,7 +212,8 @@ static TableSchema recursiveCreate(Class immutableClass, MethodHandles } // Otherwise: cache doesn't know about this class; create a new one from scratch - return create(ImmutableTableSchemaParams.builder(immutableClass).lookup(lookup).build(), metaTableSchemaCache); + return create(ImmutableTableSchemaParams.builder(immutableClass).lookup(lookup).build(), metaTableSchemaCache, + ExecutionContext.ROOT); } @@ -231,7 +249,8 @@ private static StaticImmutableTableSchema createStaticImmutableTab DynamoDbFlatten dynamoDbFlatten = getPropertyAnnotation(propertyDescriptor, DynamoDbFlatten.class); if (dynamoDbFlatten != null) { - builder.flatten(TableSchema.fromClass(propertyDescriptor.getter().getReturnType()), + builder.flatten(TableSchemaFactory.fromClass(propertyDescriptor.getter().getReturnType(), + ExecutionContext.FLATTENED), getterForProperty(propertyDescriptor, immutableClass, lookup), setterForProperty(propertyDescriptor, builderClass, lookup)); } else { @@ -466,9 +485,8 @@ private static R getPropertyAnnotation(ImmutablePropertyD } private static List propertyAnnotations(ImmutablePropertyDescriptor propertyDescriptor) { - return Stream.concat(Arrays.stream(propertyDescriptor.getter().getAnnotations()), - Arrays.stream(propertyDescriptor.setter().getAnnotations())) - .collect(Collectors.toList()); + return AnnotationUtils.expandAnnotations(propertyDescriptor.getter().getAnnotations(), + propertyDescriptor.setter().getAnnotations()); } private static AttributeConfiguration resolveAttributeConfiguration(ImmutablePropertyDescriptor propertyDescriptor) { diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/IndexValidator.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/IndexValidator.java new file mode 100644 index 000000000000..0e568ce25853 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/IndexValidator.java @@ -0,0 +1,121 @@ +/* + * 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.mapper; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; + +@SdkInternalApi +@ThreadSafe +final class IndexValidator { + + private static final int MIN_KEY_ORDER = -1; + private static final int MAX_KEY_ORDER = 3; + private static final int IMPLICIT_ORDER = -1; + + private static final String DUPLICATE_KEY_MSG = "Attempt to set an index key that conflicts with an existing " + + "index key of the same name and index. Index name: %s; " + + "attribute name: %s)"; + private static final String DUPLICATE_ATTRIBUTE_MSG = "Duplicate %s key '%s' for index '%s'"; + private static final String INVALID_ORDER_MSG = "Key order must be between %d and %d, got: %d"; + private static final String COMPOSITE_ORDERING_MSG = "Composite %s keys for index '%s' must all have explicit ordering (0," + + "1,2,3)"; + private static final String NON_COMPOSITE_ORDERING_MSG = "Invalid non-composite %s key order for index '%s'. Expected: -1,0" + + " but got: %s"; + private static final String DUPLICATE_ORDER_MSG = "Duplicate %s key order %d for index '%s'"; + private static final String NON_SEQUENTIAL_MSG = "Non-sequential %s key orders for index '%s'. Expected: 0,1,2,3 but got: %s"; + + private IndexValidator() { + } + + static void validateKeyOrder(Order order) { + if (order.getIndex() < MIN_KEY_ORDER || order.getIndex() > MAX_KEY_ORDER) { + throw new IllegalArgumentException(String.format(INVALID_ORDER_MSG, MIN_KEY_ORDER, MAX_KEY_ORDER, order.getIndex())); + } + } + + static void validateNoDuplicateKeys(List keys, + String indexName, String attributeName) { + if (keys.stream().anyMatch(k -> k.name().equals(attributeName))) { + throw new IllegalArgumentException(String.format(DUPLICATE_KEY_MSG, indexName, attributeName)); + } + } + + static void validateAllIndices(Collection indices) { + for (IndexMetadata index : indices) { + // Skip validation for primary index - composite keys only supported for GSI + if (TableMetadata.primaryIndexName().equals(index.name())) { + continue; + } + validateCompositeKeyOrdering(index.partitionKeys(), "partition", index.name()); + validateCompositeKeyOrdering(index.sortKeys(), "sort", index.name()); + } + } + + static void validateCompositeKeyOrdering(List keys, String keyType, String indexName) { + if (keys.size() <= 1) { + if (keys.size() == 1) { + int order = keys.get(0).order().getIndex(); + if (order != IMPLICIT_ORDER && order != 0) { + throw new IllegalArgumentException(String.format(NON_COMPOSITE_ORDERING_MSG, keyType, indexName, order)); + } + } + return; + } + + Set seenNames = new HashSet<>(); + Set seenOrders = new HashSet<>(); + boolean[] orderPresent = new boolean[keys.size()]; + + for (KeyAttributeMetadata key : keys) { + String name = key.name(); + int order = key.order().getIndex(); + + if (!seenNames.add(name)) { + throw new IllegalArgumentException(String.format(DUPLICATE_ATTRIBUTE_MSG, keyType, name, indexName)); + } + + if (order == IMPLICIT_ORDER) { + throw new IllegalArgumentException(String.format(COMPOSITE_ORDERING_MSG, keyType, indexName)); + } + + if (!seenOrders.add(order)) { + throw new IllegalArgumentException(String.format(DUPLICATE_ORDER_MSG, keyType, order, indexName)); + } + + if (order >= 0 && order < keys.size()) { + orderPresent[order] = true; + } + } + + for (int i = 0; i < keys.size(); i++) { + if (!orderPresent[i]) { + List actualOrders = keys.stream().map(key -> key.order().getIndex()) + .sorted() + .collect(Collectors.toList()); + throw new IllegalArgumentException(String.format(NON_SEQUENTIAL_MSG, keyType, indexName, actualOrders)); + } + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/Order.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/Order.java new file mode 100644 index 000000000000..bc030d2cd765 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/Order.java @@ -0,0 +1,37 @@ +/* + * 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.mapper; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +@SdkPublicApi +public enum Order { + UNSPECIFIED(-1), + FIRST(0), + SECOND(1), + THIRD(2), + FOURTH(3); + + private final int index; + + Order(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java index 9e3e3f2bdf2b..186cf1b2c107 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticAttributeTags.java @@ -73,6 +73,20 @@ public static StaticAttributeTag secondaryPartitionKey(String indexName) { attribute.getAttributeValueType())); } + /** + * Marks an attribute as being part of a composite partition key for a secondary index. + * + * @param indexName The name of the index this key participates in. + * @param order the order of this key in the composite key (0-based) + */ + public static StaticAttributeTag secondaryPartitionKey(String indexName, Order order) { + return new KeyAttributeTag((tableMetadataBuilder, attribute) -> + tableMetadataBuilder.addIndexPartitionKey(indexName, + attribute.getAttributeName(), + attribute.getAttributeValueType(), + order)); + } + /** * Marks an attribute as being a partition key for multiple secondary indices. * @param indexNames The names of the indices this key participates in. @@ -86,6 +100,22 @@ public static StaticAttributeTag secondaryPartitionKey(Collection indexN attribute.getAttributeValueType()))); } + /** + * Marks an attribute as being part of a composite partition key for multiple secondary indices. + * + * @param indexNames The names of the indices this key participates in. + * @param order the order of this key in the composite key (0-based) + */ + public static StaticAttributeTag secondaryPartitionKey(Collection indexNames, Order order) { + return new KeyAttributeTag( + (tableMetadataBuilder, attribute) -> + indexNames.forEach( + indexName -> tableMetadataBuilder.addIndexPartitionKey(indexName, + attribute.getAttributeName(), + attribute.getAttributeValueType(), + order))); + } + /** * Marks an attribute as being a sort key for a secondary index. * @param indexName The name of the index this key participates in. @@ -97,6 +127,20 @@ public static StaticAttributeTag secondarySortKey(String indexName) { attribute.getAttributeValueType())); } + /** + * Marks an attribute as being part of a composite sort key for a secondary index. + * + * @param indexName The name of the index this key participates in. + * @param order the order of this key in the composite key (0-based) + */ + public static StaticAttributeTag secondarySortKey(String indexName, Order order) { + return new KeyAttributeTag((tableMetadataBuilder, attribute) -> + tableMetadataBuilder.addIndexSortKey(indexName, + attribute.getAttributeName(), + attribute.getAttributeValueType(), + order)); + } + /** * Marks an attribute as being a sort key for multiple secondary indices. * @param indexNames The names of the indices this key participates in. @@ -110,6 +154,22 @@ public static StaticAttributeTag secondarySortKey(Collection indexNames) attribute.getAttributeValueType()))); } + /** + * Marks an attribute as being part of a composite sort key for multiple secondary indices. + * + * @param indexNames The names of the indices this key participates in. + * @param order the order of this key in the composite key (0-based) + */ + public static StaticAttributeTag secondarySortKey(Collection indexNames, Order order) { + return new KeyAttributeTag( + (tableMetadataBuilder, attribute) -> + indexNames.forEach( + indexName -> tableMetadataBuilder.addIndexSortKey(indexName, + attribute.getAttributeName(), + attribute.getAttributeValueType(), + order))); + } + /** * Specifies the behavior when this attribute is updated as part of an 'update' operation such as UpdateItem. See * documentation of {@link UpdateBehavior} for details on the different behaviors supported and the default diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java index 373a6e1cc0b3..2126f25dec51 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadata.java @@ -15,11 +15,13 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -48,7 +50,9 @@ public final class StaticTableMetadata implements TableMetadata { private StaticTableMetadata(Builder builder) { this.customMetadata = Collections.unmodifiableMap(builder.customMetadata); - this.indexByNameMap = Collections.unmodifiableMap(builder.indexByNameMap); + Map indices = new LinkedHashMap<>(); + builder.indexBuilders.forEach((key, value) -> indices.put(key, value.name(key).build())); + this.indexByNameMap = Collections.unmodifiableMap(indices); this.keyAttributes = Collections.unmodifiableMap(builder.keyAttributes); } @@ -79,43 +83,68 @@ public Optional customMetadataObject(String key, Class objec } @Override - public String indexPartitionKey(String indexName) { + public List indexPartitionKeys(String indexName) { IndexMetadata index = getIndex(indexName); - - if (!index.partitionKey().isPresent()) { - if (!TableMetadata.primaryIndexName().equals(indexName) && index.sortKey().isPresent()) { - // Local secondary index, use primary partition key - return primaryPartitionKey(); + + List partitionKeys = index.partitionKeys(); + if (partitionKeys.isEmpty()) { + if (!TableMetadata.primaryIndexName().equals(indexName) && !index.sortKeys().isEmpty()) { + // Local secondary index, use primary partition keys + return indexPartitionKeys(TableMetadata.primaryIndexName()); } - throw new IllegalArgumentException("Attempt to execute an operation against an index that requires a " - + "partition key without assigning a partition key to that index. " + throw new IllegalArgumentException("Attempt to execute an operation against an index that requires " + + "partition keys without assigning partition keys to that index. " + "Index name: " + indexName); } - return index.partitionKey().get().name(); + return partitionKeys.stream() + .filter(Objects::nonNull) + .map(KeyAttributeMetadata::name) + .collect(Collectors.toList()); } @Override - public Optional indexSortKey(String indexName) { + public List indexSortKeys(String indexName) { IndexMetadata index = getIndex(indexName); - - return index.sortKey().map(KeyAttributeMetadata::name); + + List sortKeys = index.sortKeys(); + return sortKeys.stream() + .filter(Objects::nonNull) + .map(KeyAttributeMetadata::name) + .collect(Collectors.toList()); } @Override public Collection indexKeys(String indexName) { IndexMetadata index = getIndex(indexName); - - if (index.sortKey().isPresent()) { - if (!TableMetadata.primaryIndexName().equals(indexName) && !index.partitionKey().isPresent()) { - // Local secondary index, use primary index for partition key - return Collections.unmodifiableList(Arrays.asList(primaryPartitionKey(), index.sortKey().get().name())); + List allKeys = new ArrayList<>(); + + List partitionKeys = index.partitionKeys(); + List sortKeys = index.sortKeys(); + + if (!sortKeys.isEmpty()) { + if (!TableMetadata.primaryIndexName().equals(indexName) && partitionKeys.isEmpty()) { + // Local secondary index, use primary index for partition keys + allKeys.addAll(indexPartitionKeys(TableMetadata.primaryIndexName())); + } else { + allKeys.addAll(partitionKeys.stream() + .filter(Objects::nonNull) + .map(KeyAttributeMetadata::name) + .collect(Collectors.toList())); } - return Collections.unmodifiableList(Arrays.asList(index.partitionKey().get().name(), index.sortKey().get().name())); + allKeys.addAll(sortKeys.stream() + .filter(Objects::nonNull) + .map(KeyAttributeMetadata::name) + .collect(Collectors.toList())); } else { - return Collections.singletonList(index.partitionKey().get().name()); + allKeys.addAll(partitionKeys.stream() + .filter(Objects::nonNull) + .map(KeyAttributeMetadata::name) + .collect(Collectors.toList())); } + + return Collections.unmodifiableList(allKeys); } @Override @@ -146,11 +175,18 @@ private IndexMetadata getIndex(String indexName) { throw new IllegalArgumentException("Attempt to execute an operation that requires a primary index " + "without defining any primary key attributes in the table " + "metadata."); - } else { - throw new IllegalArgumentException("Attempt to execute an operation that requires a secondary index " - + "without defining the index attributes in the table metadata. " - + "Index name: " + indexName); } + throw new IllegalArgumentException("Attempt to execute an operation that requires a secondary index " + + "without defining the index attributes in the table metadata. " + + "Index name: " + indexName); + } + + // Check if primary index is empty (no keys defined) + if (TableMetadata.primaryIndexName().equals(indexName) && + index.partitionKeys().isEmpty() && index.sortKeys().isEmpty()) { + throw new IllegalArgumentException("Attempt to execute an operation that requires a primary index " + + "without defining any primary key attributes in the table " + + "metadata."); } return index; @@ -201,10 +237,11 @@ public int hashCode() { @NotThreadSafe public static class Builder { private final Map customMetadata = new LinkedHashMap<>(); - private final Map indexByNameMap = new LinkedHashMap<>(); + private final Map indexBuilders = new LinkedHashMap<>(); private final Map keyAttributes = new LinkedHashMap<>(); private Builder() { + indexBuilders.put(TableMetadata.primaryIndexName(), StaticIndexMetadata.builder()); } /** @@ -271,46 +308,64 @@ public Builder addCustomMetadataObject(String key, Map objectMap * @param indexName the name of the index to associate the partition key with * @param attributeName the name of the attribute that represents the partition key * @param attributeValueType the {@link AttributeValueType} of the partition key - * @throws IllegalArgumentException if a partition key has already been defined for this index + * @param order the order of this key in composite keys (-1 for implicit, 0-3 for explicit) */ - public Builder addIndexPartitionKey(String indexName, String attributeName, AttributeValueType attributeValueType) { - IndexMetadata index = indexByNameMap.get(indexName); - - if (index != null && index.partitionKey().isPresent()) { - throw new IllegalArgumentException("Attempt to set an index partition key that conflicts with an " - + "existing index partition key of the same name and index. Index " - + "name: " + indexName + "; attribute name: " + attributeName); - } - - KeyAttributeMetadata partitionKey = StaticKeyAttributeMetadata.create(attributeName, attributeValueType); - indexByNameMap.put(indexName, - StaticIndexMetadata.builderFrom(index).name(indexName).partitionKey(partitionKey).build()); + public Builder addIndexPartitionKey(String indexName, String attributeName, + AttributeValueType attributeValueType, Order order) { + IndexValidator.validateKeyOrder(order); + StaticIndexMetadata.Builder indexBuilder = getOrCreateIndexBuilder(indexName); + IndexValidator.validateNoDuplicateKeys(indexBuilder.getPartitionKeys(), indexName, attributeName); + + KeyAttributeMetadata partitionKey = StaticKeyAttributeMetadata.create(attributeName, attributeValueType, order); + indexBuilder.addPartitionKey(partitionKey); markAttributeAsKey(attributeName, attributeValueType); return this; } + + /** + * Adds information about a partition key associated with a specific index (backward compatibility). + * @param indexName the name of the index to associate the partition key with + * @param attributeName the name of the attribute that represents the partition key + * @param attributeValueType the {@link AttributeValueType} of the partition key + */ + public Builder addIndexPartitionKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + return addIndexPartitionKey(indexName, attributeName, attributeValueType, Order.UNSPECIFIED); + } /** * Adds information about a sort key associated with a specific index. * @param indexName the name of the index to associate the sort key with * @param attributeName the name of the attribute that represents the sort key * @param attributeValueType the {@link AttributeValueType} of the sort key - * @throws IllegalArgumentException if a sort key has already been defined for this index + * @param order the order of this key in composite keys (-1 for implicit, 0-3 for explicit) */ - public Builder addIndexSortKey(String indexName, String attributeName, AttributeValueType attributeValueType) { - IndexMetadata index = indexByNameMap.get(indexName); - - if (index != null && index.sortKey().isPresent()) { - throw new IllegalArgumentException("Attempt to set an index sort key that conflicts with an existing" - + " index sort key of the same name and index. Index name: " - + indexName + "; attribute name: " + attributeName); - } - - KeyAttributeMetadata sortKey = StaticKeyAttributeMetadata.create(attributeName, attributeValueType); - indexByNameMap.put(indexName, - StaticIndexMetadata.builderFrom(index).name(indexName).sortKey(sortKey).build()); + public Builder addIndexSortKey(String indexName, String attributeName, + AttributeValueType attributeValueType, Order order) { + IndexValidator.validateKeyOrder(order); + StaticIndexMetadata.Builder indexBuilder = getOrCreateIndexBuilder(indexName); + IndexValidator.validateNoDuplicateKeys(indexBuilder.getSortKeys(), indexName, attributeName); + + KeyAttributeMetadata sortKey = StaticKeyAttributeMetadata.create(attributeName, attributeValueType, order); + indexBuilder.addSortKey(sortKey); markAttributeAsKey(attributeName, attributeValueType); return this; } + + /** + * Adds information about a non-composite sort key associated with a specific index. + * @param indexName the name of the index to associate the sort key with + * @param attributeName the name of the attribute that represents the sort key + * @param attributeValueType the {@link AttributeValueType} of the sort key + */ + public Builder addIndexSortKey(String indexName, String attributeName, AttributeValueType attributeValueType) { + return addIndexSortKey(indexName, attributeName, attributeValueType, Order.UNSPECIFIED); + } + + private StaticIndexMetadata.Builder getOrCreateIndexBuilder(String indexName) { + return indexBuilders.computeIfAbsent(indexName, k -> StaticIndexMetadata.builder()); + } + + /** * Declares a 'key-like' attribute that is not an actual DynamoDB key. These pseudo-keys can then be recognized @@ -341,14 +396,18 @@ public Builder markAttributeAsKey(String attributeName, AttributeValueType attri Builder mergeWith(TableMetadata other) { other.indices().forEach( index -> { - index.partitionKey().ifPresent( - partitionKey -> addIndexPartitionKey(index.name(), - partitionKey.name(), - partitionKey.attributeValueType())); - - index.sortKey().ifPresent( - sortKey -> addIndexSortKey(index.name(), sortKey.name(), sortKey.attributeValueType()) - ); + for (KeyAttributeMetadata partitionKey : index.partitionKeys()) { + addIndexPartitionKey(index.name(), + partitionKey.name(), + partitionKey.attributeValueType(), + partitionKey.order()); + } + for (KeyAttributeMetadata sortKey : index.sortKeys()) { + addIndexSortKey(index.name(), + sortKey.name(), + sortKey.attributeValueType(), + sortKey.order()); + } }); other.customMetadata().forEach(this::mergeCustomMetaDataObject); diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java index 215631902acc..a508dc7dee06 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchema.java @@ -31,6 +31,7 @@ import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.DefaultAttributeConverterProvider; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; +import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; /** @@ -277,7 +278,19 @@ public Builder attributeConverterProviders(List a * Builds a {@link StaticTableSchema} based on the values this builder has been configured with */ public StaticTableSchema build() { - return new StaticTableSchema<>(this); + return build(ExecutionContext.ROOT); + } + + /** + * Builds a {@link StaticTableSchema} based on the values this builder has been configured with + * @param context The context in which the schema is being executed, from root or from a flatten operation. + */ + public StaticTableSchema build(ExecutionContext context) { + StaticTableSchema staticTableSchema = new StaticTableSchema<>(this); + if (context == ExecutionContext.ROOT) { + IndexValidator.validateAllIndices(staticTableSchema.tableMetadata().indices()); + } + return staticTableSchema; } } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java new file mode 100644 index 000000000000..e1021383e68c --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/TableSchemaFactory.java @@ -0,0 +1,42 @@ +/* + * 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.mapper; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; + +@SdkInternalApi +public class TableSchemaFactory { + + private TableSchemaFactory() { + } + + public static TableSchema fromClass(Class annotatedClass, ExecutionContext context) { + if (annotatedClass.getAnnotation(DynamoDbImmutable.class) != null) { + return ImmutableTableSchema.create(annotatedClass, context); + } + + if (annotatedClass.getAnnotation(DynamoDbBean.class) != null) { + return BeanTableSchema.create(annotatedClass, context); + } + + throw new IllegalArgumentException("Class does not appear to be a valid DynamoDb annotated class. [class = " + + "\"" + annotatedClass + "\"]"); + } +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKey.java index 3b5bd2a183db..450f4671c8f5 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKey.java @@ -16,11 +16,13 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper.annotations; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; /** * Denotes a partition key for a global secondary index. @@ -34,9 +36,16 @@ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class) +@Repeatable(DynamoDbSecondaryPartitionKeys.class) public @interface DynamoDbSecondaryPartitionKey { /** * The names of one or more global secondary indices that this partition key should participate in. */ String[] indexNames(); + + /** + * The order of this partition key attribute in composite keys (0-3). + * Required when multiple partition keys are defined. + */ + Order order() default Order.UNSPECIFIED; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKeys.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKeys.java new file mode 100644 index 000000000000..17a79214d4e8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondaryPartitionKeys.java @@ -0,0 +1,31 @@ +/* + * 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.mapper.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; + +@SdkPublicApi +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface DynamoDbSecondaryPartitionKeys { + DynamoDbSecondaryPartitionKey[] value(); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKey.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKey.java index 9fdee8065196..900ebb6abf31 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKey.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKey.java @@ -16,11 +16,13 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper.annotations; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.BeanTableSchemaAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; /** * Denotes an optional sort key for a global or local secondary index. @@ -36,6 +38,7 @@ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @BeanTableSchemaAttributeTag(BeanTableSchemaAttributeTags.class) +@Repeatable(DynamoDbSecondarySortKeys.class) public @interface DynamoDbSecondarySortKey { /** * The names of one or more local or global secondary indices that this sort key should participate in. @@ -45,4 +48,10 @@ * secondary indexes. */ String[] indexNames(); + + /** + * The order of this sort key attribute in composite keys (0-3). + * Required when multiple sort keys are defined. + */ + Order order() default Order.UNSPECIFIED; } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKeys.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKeys.java new file mode 100644 index 000000000000..832817a19cee --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/mapper/annotations/DynamoDbSecondarySortKeys.java @@ -0,0 +1,31 @@ +/* + * 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.mapper.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import software.amazon.awssdk.annotations.SdkPublicApi; + +@SdkPublicApi +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface DynamoDbSecondarySortKeys { + DynamoDbSecondarySortKey[] value(); +} diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/BatchGetResultPage.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/BatchGetResultPage.java index adec895c0cf1..c557a4f1e463 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/BatchGetResultPage.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/BatchGetResultPage.java @@ -18,10 +18,9 @@ import static java.util.Collections.emptyList; import static software.amazon.awssdk.enhanced.dynamodb.internal.EnhancedClientUtils.readAndTransformSingleItem; -import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.stream.Collectors; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -30,6 +29,7 @@ import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.MappedTableResource; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.internal.operations.DefaultOperationContext; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse; @@ -94,21 +94,32 @@ public List unprocessedKeysForTable(MappedTableResource mappedTable) { KeysAndAttributes keysAndAttributes = this.batchGetItemResponse.unprocessedKeys().get(mappedTable.tableName()); if (keysAndAttributes == null) { - return Collections.emptyList(); + return emptyList(); } - String partitionKey = mappedTable.tableSchema().tableMetadata().primaryPartitionKey(); - Optional sortKey = mappedTable.tableSchema().tableMetadata().primarySortKey(); + List partitionKeys = mappedTable.tableSchema().tableMetadata() + .indexPartitionKeys(TableMetadata.primaryIndexName()); + List sortKeys = mappedTable.tableSchema().tableMetadata() + .indexSortKeys(TableMetadata.primaryIndexName()); return keysAndAttributes.keys() .stream() .map(keyMap -> { - AttributeValue partitionValue = keyMap.get(partitionKey); - AttributeValue sortValue = sortKey.map(keyMap::get).orElse(null); + List partitionValues = partitionKeys.stream() + .map(keyMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + List sortValues = sortKeys.stream() + .map(keyMap::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return Key.builder() - .partitionValue(partitionValue) - .sortValue(sortValue) - .build(); }) + .partitionValues(partitionValues) + .sortValues(sortValues) + .build(); + }) .collect(Collectors.toList()); } diff --git a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.java b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.java index ced68bbe4c61..dca1958a660a 100644 --- a/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.java +++ b/services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/model/QueryConditional.java @@ -43,6 +43,11 @@ public interface QueryConditional { /** * Creates a {@link QueryConditional} that matches when the key of an index is equal to a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and "=" operator will be applied on all of them. + * The sort keys are optional but if are provided, "=" operator will be applied only to the provided ones. * @param key the literal key used to compare the value of the index against */ static QueryConditional keyEqualTo(Key key) { @@ -51,6 +56,11 @@ static QueryConditional keyEqualTo(Key key) { /** * Creates a {@link QueryConditional} that matches when the key of an index is equal to a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and "=" operator will be applied on all of them. + * The sort keys are optional but if are provided, "=" operator will be applied only to the provided ones. * @param keyConsumer 'builder consumer' for the literal key used to compare the value of the index against */ static QueryConditional keyEqualTo(Consumer keyConsumer) { @@ -61,6 +71,12 @@ static QueryConditional keyEqualTo(Consumer keyConsumer) { /** * Creates a {@link QueryConditional} that matches when the key of an index is greater than a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the ">" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param key the literal key used to compare the value of the index against */ static QueryConditional sortGreaterThan(Key key) { @@ -69,6 +85,12 @@ static QueryConditional sortGreaterThan(Key key) { /** * Creates a {@link QueryConditional} that matches when the key of an index is greater than a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the ">" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param keyConsumer 'builder consumer' for the literal key used to compare the value of the index against */ static QueryConditional sortGreaterThan(Consumer keyConsumer) { @@ -80,6 +102,12 @@ static QueryConditional sortGreaterThan(Consumer keyConsumer) { /** * Creates a {@link QueryConditional} that matches when the key of an index is greater than or equal to a specific * value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the ">=" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param key the literal key used to compare the value of the index against */ static QueryConditional sortGreaterThanOrEqualTo(Key key) { @@ -89,6 +117,12 @@ static QueryConditional sortGreaterThanOrEqualTo(Key key) { /** * Creates a {@link QueryConditional} that matches when the key of an index is greater than or equal to a specific * value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the ">=" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param keyConsumer 'builder consumer' for the literal key used to compare the value of the index against */ static QueryConditional sortGreaterThanOrEqualTo(Consumer keyConsumer) { @@ -99,6 +133,12 @@ static QueryConditional sortGreaterThanOrEqualTo(Consumer keyConsum /** * Creates a {@link QueryConditional} that matches when the key of an index is less than a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "<" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param key the literal key used to compare the value of the index against */ static QueryConditional sortLessThan(Key key) { @@ -107,6 +147,12 @@ static QueryConditional sortLessThan(Key key) { /** * Creates a {@link QueryConditional} that matches when the key of an index is less than a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "<" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param keyConsumer 'builder consumer' for the literal key used to compare the value of the index against */ static QueryConditional sortLessThan(Consumer keyConsumer) { @@ -118,6 +164,12 @@ static QueryConditional sortLessThan(Consumer keyConsumer) { /** * Creates a {@link QueryConditional} that matches when the key of an index is less than or equal to a specific * value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "<=" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param key the literal key used to compare the value of the index against */ static QueryConditional sortLessThanOrEqualTo(Key key) { @@ -127,6 +179,12 @@ static QueryConditional sortLessThanOrEqualTo(Key key) { /** * Creates a {@link QueryConditional} that matches when the key of an index is less than or equal to a specific * value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "<=" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param keyConsumer 'builder consumer' for the literal key used to compare the value of the index against */ static QueryConditional sortLessThanOrEqualTo(Consumer keyConsumer) { @@ -137,6 +195,12 @@ static QueryConditional sortLessThanOrEqualTo(Consumer keyConsumer) /** * Creates a {@link QueryConditional} that matches when the key of an index is between two specific values. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "between" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param keyFrom the literal key used to compare the start of the range to compare the value of the index against * @param keyTo the literal key used to compare the end of the range to compare the value of the index against */ @@ -146,6 +210,12 @@ static QueryConditional sortBetween(Key keyFrom, Key keyTo) { /** * Creates a {@link QueryConditional} that matches when the key of an index is between two specific values. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "between" operator will be applied only to the rightmost provided one, but all the preceding sort + * keys must also be provided and equality condition will be applied on them. * @param keyFromConsumer 'builder consumer' for the literal key used to compare the start of the range to compare * the value of the index against * @param keyToConsumer 'builder consumer' for the literal key used to compare the end of the range to compare the @@ -161,6 +231,12 @@ static QueryConditional sortBetween(Consumer keyFromConsumer, Consu /** * Creates a {@link QueryConditional} that matches when the key of an index begins with a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "begins_with" operator will be applied only to the rightmost provided one, but all the preceding + * sort keys must also be provided and equality condition will be applied on them. * @param key the literal key used to compare the start of the value of the index against */ static QueryConditional sortBeginsWith(Key key) { @@ -169,6 +245,12 @@ static QueryConditional sortBeginsWith(Key key) { /** * Creates a {@link QueryConditional} that matches when the key of an index begins with a specific value. + * Supports both single keys and composite keys with up to {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} + * partition and {@value software.amazon.awssdk.enhanced.dynamodb.Key#MAX_KEYS} sort keys. + *

+ * In case of composite keys, all the partition keys must be provided and equality condition will be applied on all of them. + * For the sort keys, the "begins_with" operator will be applied only to the rightmost provided one, but all the preceding + * sort keys must also be provided and equality condition will be applied on them. * @param keyConsumer 'builder consumer' the literal key used to compare the start of the value of the index * against */ diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/KeyTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/KeyTest.java index ec47e04cec69..4ac2c3d8014d 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/KeyTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/KeyTest.java @@ -21,11 +21,15 @@ import static org.hamcrest.Matchers.is; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithCompositeGsi; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithIndices; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @@ -128,4 +132,426 @@ public void nullPartitionKey_shouldThrowException() { assertThatThrownBy(() -> Key.builder().partitionValue(AttributeValue.builder().nul(true).build()).build()) .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("partitionValue should not be null"); } -} + + @Test + public void compositePartitionKeys_buildsCorrectly() { + List partitionValues = Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build() + ); + + Key key = Key.builder().partitionValues(partitionValues).build(); + + assertThat(key.partitionKeyValues(), is(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build() + ))); + assertThat(key.sortKeyValues(), is(Collections.emptyList())); + } + + @Test + public void compositeSortKeys_buildsCorrectly() { + List sortValues = Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build() + ); + + Key key = Key.builder() + .partitionValue("pk1") + .sortValues(sortValues) + .build(); + + assertThat(key.partitionKeyValues(), is(Collections.singletonList( + AttributeValue.builder().s("pk1").build() + ))); + + assertThat(key.sortKeyValues(), is(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build() + ))); + } + + @Test + public void compositeKeys_maxFourPartitionKeys() { + List partitionValues = Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build() + ); + + Key key = Key.builder().partitionValues(partitionValues).build(); + assertThat(key.partitionKeyValues().size(), is(4)); + } + + @Test + public void compositeKeys_exceedingMaxPartitionKeys_throwsException() { + List partitionValues = Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build(), + AttributeValue.builder().s("pk5").build() + ); + + assertThatThrownBy(() -> Key.builder().partitionValues(partitionValues).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Maximum 4 partition keys supported"); + } + + @Test + public void compositeKeys_maxFourSortKeys() { + List sortValues = Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("sk4").build() + ); + + Key key = Key.builder() + .partitionValue("pk1") + .sortValues(sortValues) + .build(); + assertThat(key.sortKeyValues().size(), is(4)); + } + + @Test + public void compositeKeys_exceedingMaxSortKeys_throwsException() { + List sortValues = Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("sk4").build(), + AttributeValue.builder().s("sk5").build() + ); + + assertThatThrownBy(() -> Key.builder().partitionValue("pk1").sortValues(sortValues).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Maximum 4 sort keys supported"); + } + + @Test + public void compositeKeys_emptyPartitionValues_throwsException() { + assertThatThrownBy(() -> Key.builder().partitionValues(Collections.emptyList()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("partitionValues should not be null or empty"); + } + + @Test + public void compositeKeys_nullPartitionValues_throwsException() { + assertThatThrownBy(() -> Key.builder().partitionValues(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("partitionValues should not be null or empty"); + } + + @Test + public void compositeKeys_nullSortValues_createsEmptyList() { + Key key = Key.builder() + .partitionValue("pk1") + .sortValues(null) + .build(); + + assertThat(key.sortKeyValues(), is(Collections.emptyList())); + } + + @Test + public void compositeKeys_backwardCompatibility_partitionValue() { + Key key = Key.builder().partitionValue("pk1").build(); + + assertThat(key.partitionKeyValues(), is(Collections.singletonList( + AttributeValue.builder().s("pk1").build() + ))); + + assertThat(key.partitionKeyValue(), is( + AttributeValue.builder().s("pk1").build() + )); + } + + @Test + public void compositeKeys_backwardCompatibility_sortValue() { + Key key = Key.builder() + .partitionValue("pk1") + .sortValue("sk1") + .build(); + + assertThat(key.sortKeyValues(), is(Collections.singletonList( + AttributeValue.builder().s("sk1").build() + ))); + + assertThat(key.sortKeyValue(), is( + Optional.of(AttributeValue.builder().s("sk1").build()) + )); + } + + @Test + public void compositeKeys_toBuilder_preservesValues() { + List partitionValues = Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build() + ); + List sortValues = Collections.singletonList( + AttributeValue.builder().s("sk1").build() + ); + + Key original = Key.builder() + .partitionValues(partitionValues) + .sortValues(sortValues) + .build(); + + Key rebuilt = original.toBuilder().build(); + + assertThat(rebuilt.partitionKeyValues(), is(original.partitionKeyValues())); + assertThat(rebuilt.sortKeyValues(), is(original.sortKeyValues())); + } + + @Test + public void compositeKeys_keyMap_withCompositeGsi() { + List partitionValues = Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build() + ); + + List sortValues = Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build() + ); + + Key key = Key.builder() + .partitionValues(partitionValues) + .sortValues(sortValues) + .build(); + + Map keyMap = key.keyMap(FakeItemWithCompositeGsi.getTableSchema(), "composite_gsi"); + + assertThat(keyMap.size(), is(4)); + assertThat(keyMap.get("gsi_pk1"), is(AttributeValue.builder().s("pk1").build())); + assertThat(keyMap.get("gsi_pk2"), is(AttributeValue.builder().s("pk2").build())); + assertThat(keyMap.get("gsi_sk1"), is(AttributeValue.builder().s("sk1").build())); + assertThat(keyMap.get("gsi_sk2"), is(AttributeValue.builder().s("sk2").build())); + } + + @Test + public void compositeKeys_keyMap_moreValuesThanKeys_usesAvailableKeys() { + List partitionValues = Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), // Extra value + AttributeValue.builder().s("pk3").build() // Extra value + ); + + Key key = Key.builder().partitionValues(partitionValues).build(); + + Map keyMap = key.keyMap(FakeItemWithIndices.getTableSchema(), "gsi_1"); + + assertThat(keyMap.size(), is(1)); + assertThat(keyMap.get("gsi_id"), is(AttributeValue.builder().s("pk1").build())); + } + + @Test + public void compositeKeys_keyMap_sortValuesWithoutSortKeys_throwsException() { + Key key = Key.builder() + .partitionValue("pk1") + .sortValues(Collections.singletonList(AttributeValue.builder().s("sk1").build())) + .build(); + + assertThatThrownBy(() -> key.keyMap(FakeItemWithIndices.getTableSchema(), "gsi_2")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Sort key values were supplied for an index that does not support sort keys"); + } + + @Test + public void addPartitionValuesFromStrings_convertsCorrectly() { + Key key = Key.builder().addPartitionValue("pk1") + .addPartitionValue("pk2") + .addPartitionValue("pk3") + .build(); + + assertThat(key.partitionKeyValues(), is(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build() + ))); + assertThat(key.partitionKeyValue(), is(AttributeValue.builder().s("pk1").build())); + } + + @Test + public void addPartitionValue_null_throwsException() { + assertThatThrownBy(() -> Key.builder().addPartitionValue(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Partition key value cannot be null"); + } + + @Test + public void addSortValuesFromStrings_convertsCorrectly() { + Key key = Key.builder() + .partitionValue("pk1") + .addSortValue("sk1") + .addSortValue("sk2") + .build(); + + assertThat(key.sortKeyValues(), is(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build() + ))); + assertThat(key.sortKeyValue(), is(Optional.of(AttributeValue.builder().s("sk1").build()))); + } + + @Test + public void addPartitionValuesFromNumbers_convertsCorrectly() { + Key key = Key.builder() + .addPartitionValue(123) + .addPartitionValue(45.6) + .addPartitionValue(789L) + .build(); + + assertThat(key.partitionKeyValues(), is(Arrays.asList( + AttributeValue.builder().n("123").build(), + AttributeValue.builder().n("45.6").build(), + AttributeValue.builder().n("789").build() + ))); + assertThat(key.partitionKeyValue(), is(AttributeValue.builder().n("123").build())); + } + + @Test + public void addSortValuesFromNumbers_convertsCorrectly() { + Key key = Key.builder() + .partitionValue("pk1") + .addSortValue(100) + .addSortValue(200.5) + .build(); + + assertThat(key.sortKeyValues(), is(Arrays.asList( + AttributeValue.builder().n("100").build(), + AttributeValue.builder().n("200.5").build() + ))); + assertThat(key.sortKeyValue(), is(Optional.of(AttributeValue.builder().n("100").build()))); + } + + @Test + public void addPartitionValuesFromBinary_convertsCorrectly() { + SdkBytes bytes1 = SdkBytes.fromString("data1", StandardCharsets.UTF_8); + SdkBytes bytes2 = SdkBytes.fromString("data2", StandardCharsets.UTF_8); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .build(); + + assertThat(key.partitionKeyValues(), is(Arrays.asList( + AttributeValue.builder().b(bytes1).build(), + AttributeValue.builder().b(bytes2).build() + ))); + assertThat(key.partitionKeyValue(), is(AttributeValue.builder().b(bytes1).build())); + } + + @Test + public void addSortValuesFromBinary_convertsCorrectly() { + SdkBytes bytes1 = SdkBytes.fromString("sort1", StandardCharsets.UTF_8); + SdkBytes bytes2 = SdkBytes.fromString("sort2", StandardCharsets.UTF_8); + + Key key = Key.builder() + .partitionValue("pk1") + .addSortValue(bytes1) + .addSortValue(bytes2) + .build(); + + assertThat(key.sortKeyValues(), is(Arrays.asList( + AttributeValue.builder().b(bytes1).build(), + AttributeValue.builder().b(bytes2).build() + ))); + assertThat(key.sortKeyValue(), is(Optional.of(AttributeValue.builder().b(bytes1).build()))); + } + + @Test + public void addCompositeKeys_mixedTypes_partitionFromStrings_sortFromNumbers() { + Key key = Key.builder() + .addPartitionValue("tenant1") + .addPartitionValue("region1") + .addSortValue(2023) + .addSortValue(1) + .build(); + + assertThat(key.partitionKeyValues(), is(Arrays.asList( + AttributeValue.builder().s("tenant1").build(), + AttributeValue.builder().s("region1").build() + ))); + assertThat(key.sortKeyValues(), is(Arrays.asList( + AttributeValue.builder().n("2023").build(), + AttributeValue.builder().n("1").build() + ))); + } + + @Test + public void addCompositeKeys_mixedTypes_partitionFromNumbers_sortFromBinary() { + SdkBytes sortBytes = SdkBytes.fromString("sortdata", StandardCharsets.UTF_8); + + Key key = Key.builder() + .addPartitionValue(100) + .addPartitionValue(200) + .addSortValue(sortBytes) + .build(); + + assertThat(key.partitionKeyValues(), is(Arrays.asList( + AttributeValue.builder().n("100").build(), + AttributeValue.builder().n("200").build() + ))); + assertThat(key.sortKeyValues(), is(Collections.singletonList( + AttributeValue.builder().b(sortBytes).build() + ))); + } + + @Test + public void addCompositeKeys_fromStrings_maxFourPartitionKeys() { + Key key = Key.builder() + .addPartitionValue("pk1") + .addPartitionValue("pk2") + .addPartitionValue("pk3") + .addPartitionValue("pk4") + .build(); + assertThat(key.partitionKeyValues().size(), is(4)); + } + + @Test + public void addCompositeKeys_moreThanMaxPartitionKeys_throwsException() { + assertThatThrownBy(() -> Key.builder() + .addPartitionValue("pk1") + .addPartitionValue("pk2") + .addPartitionValue("pk3") + .addPartitionValue("pk4") + .addPartitionValue("pk5") + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Maximum 4 partition keys supported"); + } + + @Test + public void addCompositeKeys_backwardCompatibility() { + Key key = Key.builder() + .addPartitionValue("pk1") + .addPartitionValue("pk2") + .addSortValue("sk1") + .build(); + + assertThat(key.partitionKeyValues().get(0), is(AttributeValue.builder().s("pk1").build())); + assertThat(key.partitionKeyValues().get(1), is(AttributeValue.builder().s("pk2").build())); + assertThat(key.sortKeyValues().get(0), is(AttributeValue.builder().s("sk1").build())); + + assertThat(key.partitionKeyValue(), is(AttributeValue.builder().s("pk1").build())); + assertThat(key.sortKeyValue(), is(Optional.of(AttributeValue.builder().s("sk1").build()))); + } + + @Test + public void addCompositeKeys_toBuilder_preservesValues() { + Key original = Key.builder() + .addPartitionValue("pk1") + .addPartitionValue("pk2") + .addSortValue("sk1") + .build(); + + Key rebuilt = original.toBuilder().build(); + + assertThat(rebuilt.partitionKeyValues(), is(original.partitionKeyValues())); + assertThat(rebuilt.sortKeyValues(), is(original.sortKeyValues())); + assertThat(rebuilt.partitionKeyValue(), is(original.partitionKeyValue())); + assertThat(rebuilt.sortKeyValue(), is(original.sortKeyValue())); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java new file mode 100644 index 000000000000..5afe4e9b52c2 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableMetadataCompositeKeyTest.java @@ -0,0 +1,80 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithIndices; + +class TableMetadataCompositeKeyTest { + + private static final TableSchema SIMPLE_SCHEMA = FakeItem.getTableSchema(); + private static final TableSchema INDEXED_SCHEMA = FakeItemWithIndices.getTableSchema(); + + @Test + void indexPartitionKeys_primaryIndex_returnsSingleKey() { + TableMetadata metadata = SIMPLE_SCHEMA.tableMetadata(); + + List partitionKeys = metadata.indexPartitionKeys(TableMetadata.primaryIndexName()); + + assertThat(partitionKeys).containsExactly("id"); + } + + @Test + void indexSortKeys_primaryIndexNoSort_returnsEmptyList() { + TableMetadata metadata = SIMPLE_SCHEMA.tableMetadata(); + + List sortKeys = metadata.indexSortKeys(TableMetadata.primaryIndexName()); + + assertThat(sortKeys).isEmpty(); + } + + @Test + void indexPartitionKeys_gsiIndex_returnsSingleKey() { + TableMetadata metadata = INDEXED_SCHEMA.tableMetadata(); + + List partitionKeys = metadata.indexPartitionKeys("gsi_1"); + + assertThat(partitionKeys).containsExactly("gsi_id"); + } + + @Test + void backwardCompatibility_deprecatedMethods_stillWork() { + TableMetadata metadata = INDEXED_SCHEMA.tableMetadata(); + + String partitionKey = metadata.indexPartitionKey(TableMetadata.primaryIndexName()); + assertThat(partitionKey).isEqualTo("id"); + + Optional sortKey = metadata.indexSortKey(TableMetadata.primaryIndexName()); + assertThat(sortKey).isPresent(); + assertThat(sortKey.get()).isEqualTo("sort"); + } + + @Test + void backwardCompatibility_newMethodsMatchDeprecated() { + TableMetadata metadata = INDEXED_SCHEMA.tableMetadata(); + + String deprecatedPartitionKey = metadata.indexPartitionKey("gsi_1"); + List newPartitionKeys = metadata.indexPartitionKeys("gsi_1"); + + assertThat(newPartitionKeys).hasSize(1); + assertThat(newPartitionKeys.get(0)).isEqualTo(deprecatedPartitionKey); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java index daac73362923..198b4a653577 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/TableSchemaTest.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; +import java.util.Optional; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -25,9 +27,18 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema; import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeMetadataBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CrossIndexBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DuplicateOrderBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ImplicitOrderBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedOrderingBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MultiGSIBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NonSequentialOrderBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.OrderPreservationBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SingleKeyBean; public class TableSchemaTest { @Rule @@ -85,10 +96,146 @@ public void fromClass_constructsImmutableTableSchema() { assertThat(tableSchema).isInstanceOf(ImmutableTableSchema.class); } + @Test + public void fromBean_constructsTableMetadata_withGSICompositeKeys() { + TableSchema schema = TableSchema.fromBean(CompositeMetadataBean.class); + TableMetadata metadata = schema.tableMetadata(); + + assertThat(metadata.indexPartitionKey(TableMetadata.primaryIndexName())).isEqualTo("id"); + assertThat(metadata.indexSortKey(TableMetadata.primaryIndexName())).isEqualTo(Optional.of("sort")); + + List gsiPartitionKeys = metadata.indexPartitionKeys("gsi1"); + assertThat(gsiPartitionKeys).containsExactly("gsiPk1", "gsiPk2"); + + List gsiSortKeys = metadata.indexSortKeys("gsi1"); + assertThat(gsiSortKeys).containsExactly("gsiSk1", "gsiSk2"); + } + + @Test + public void fromBean_constructsTableMetadata_withGSICompositePartitionKeys_AndOrderPreserved() { + TableSchema schema = TableSchema.fromBean(OrderPreservationBean.class); + TableMetadata metadata = schema.tableMetadata(); + + Optional gsi1Metadata = metadata.indices().stream() + .filter(index -> "gsi1".equals(index.name())) + .findFirst(); + + assertThat(gsi1Metadata.isPresent()).isTrue(); + + List partitionKeysMetadata = gsi1Metadata.get().partitionKeys(); + assertThat(partitionKeysMetadata.size()).isEqualTo(4); + + assertThat(partitionKeysMetadata.get(0).name()).isEqualTo("key3"); + assertThat(partitionKeysMetadata.get(0).order().getIndex()).isEqualTo(0); + assertThat(partitionKeysMetadata.get(0).attributeValueType()).isEqualTo(AttributeValueType.S); + + assertThat(partitionKeysMetadata.get(1).name()).isEqualTo("key2"); + assertThat(partitionKeysMetadata.get(1).order().getIndex()).isEqualTo(1); + assertThat(partitionKeysMetadata.get(1).attributeValueType()).isEqualTo(AttributeValueType.S); + + assertThat(partitionKeysMetadata.get(2).name()).isEqualTo("key4"); + assertThat(partitionKeysMetadata.get(2).order().getIndex()).isEqualTo(2); + assertThat(partitionKeysMetadata.get(2).attributeValueType()).isEqualTo(AttributeValueType.S); + + assertThat(partitionKeysMetadata.get(3).name()).isEqualTo("key1"); + assertThat(partitionKeysMetadata.get(3).order().getIndex()).isEqualTo(3); + assertThat(partitionKeysMetadata.get(3).attributeValueType()).isEqualTo(AttributeValueType.S); + } + + @Test + public void fromBean_constructsTableMetadata_withGSICompositeKeys_crossIndexConsistency() { + TableSchema schema = TableSchema.fromBean(CrossIndexBean.class); + + List gsi1PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi1"); + assertThat(gsi1PartitionKeys.size()).isEqualTo(2); + assertThat(gsi1PartitionKeys).containsExactly("attr1", "attr2"); + + List gsi2PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi2"); + assertThat(gsi2PartitionKeys.size()).isEqualTo(1); + assertThat(gsi2PartitionKeys).containsExactly("attr3"); + + List gsi2SortKeys = schema.tableMetadata().indexSortKeys("gsi2"); + assertThat(gsi2SortKeys.size()).isEqualTo(1); + assertThat(gsi2SortKeys).containsExactly("attr1"); + } + + @Test + public void fromBean_constructsTableMetadata_withGSISingleKeys_backwardCompatibilityMethods() { + TableSchema schema = TableSchema.fromBean(SingleKeyBean.class); + TableMetadata metadata = schema.tableMetadata(); + + assertThat(metadata.indexPartitionKey("gsi1")).isEqualTo("gsiPk"); + assertThat(metadata.indexSortKey("gsi1")).isEqualTo(Optional.of("gsiSk")); + + List partitionKeys = metadata.indexPartitionKeys("gsi1"); + assertThat(partitionKeys.size()).isEqualTo(1); + assertThat(partitionKeys).containsExactly("gsiPk"); + + List sortKeys = metadata.indexSortKeys("gsi1"); + assertThat(sortKeys.size()).isEqualTo(1); + assertThat(sortKeys).containsExactly("gsiSk"); + } + + @Test + public void fromBean_constructsTableMetadata_withMultipleGSI_differentCompositeStructures() { + TableSchema schema = TableSchema.fromBean(MultiGSIBean.class); + + List gsi1PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi1"); + assertThat(gsi1PartitionKeys.size()).isEqualTo(2); + assertThat(gsi1PartitionKeys).containsExactly("gsi1Pk1", "gsi1Pk2"); + + List gsi1SortKeys = schema.tableMetadata().indexSortKeys("gsi1"); + assertThat(gsi1SortKeys.size()).isEqualTo(1); + assertThat(gsi1SortKeys).containsExactly("gsi1Sk"); + + List gsi2PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi2"); + assertThat(gsi2PartitionKeys.size()).isEqualTo(1); + assertThat(gsi2PartitionKeys).containsExactly("gsi2Pk"); + + List gsi2SortKeys = schema.tableMetadata().indexSortKeys("gsi2"); + assertThat(gsi2SortKeys.size()).isEqualTo(2); + assertThat(gsi2SortKeys).containsExactly("gsi2Sk1", "gsi2Sk2"); + + List gsi3PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi3"); + assertThat(gsi3PartitionKeys.size()).isEqualTo(3); + assertThat(gsi3PartitionKeys).containsExactly("gsi3Pk1", "gsi3Pk2", "gsi3Pk3"); + + List gsi3SortKeys = schema.tableMetadata().indexSortKeys("gsi3"); + assertThat(gsi3SortKeys.size()).isEqualTo(0); + } + @Test public void fromClass_invalidClassThrowsException() { exception.expect(IllegalArgumentException.class); exception.expectMessage("InvalidBean"); TableSchema.fromClass(InvalidBean.class); } + + @Test + public void fromBean_schemaGeneration_GSICompositeKeyImplicitOrdering_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Composite partition keys for index 'gsi1' must all have explicit ordering (0,1,2,3)"); + TableSchema.fromClass(ImplicitOrderBean.class); + } + + @Test + public void fromBean_schemaGeneration_GSICompositeKeyDuplicateOrderValues_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Duplicate partition key order"); + TableSchema.fromBean(DuplicateOrderBean.class); + } + + @Test + public void fromBean_schemaGeneration_GSICompositeKeyNonSequentialOrders_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Non-sequential partition key orders"); + TableSchema.fromBean(NonSequentialOrderBean.class); + } + + @Test + public void fromBean_schemaGeneration_GSICompositeKeyMixedExplicitImplicit_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("must all have explicit ordering"); + TableSchema.fromBean(MixedOrderingBean.class); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/CompositeKeyFakeItem.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/CompositeKeyFakeItem.java new file mode 100644 index 000000000000..6b535afdf7ca --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/CompositeKeyFakeItem.java @@ -0,0 +1,168 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; + +public class CompositeKeyFakeItem { + private String id; + private String gsiKey1; + private String gsiKey2; + private String gsiSort1; + private String gsiSort2; + + public CompositeKeyFakeItem() { + } + + private CompositeKeyFakeItem(Builder builder) { + this.id = builder.id; + this.gsiKey1 = builder.gsiKey1; + this.gsiKey2 = builder.gsiKey2; + this.gsiSort1 = builder.gsiSort1; + this.gsiSort2 = builder.gsiSort2; + } + + public static Builder builder() { + return new Builder(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getGsiKey1() { + return gsiKey1; + } + + public void setGsiKey1(String gsiKey1) { + this.gsiKey1 = gsiKey1; + } + + public String getGsiKey2() { + return gsiKey2; + } + + public void setGsiKey2(String gsiKey2) { + this.gsiKey2 = gsiKey2; + } + + public String getGsiSort1() { + return gsiSort1; + } + + public void setGsiSort1(String gsiSort1) { + this.gsiSort1 = gsiSort1; + } + + public String getGsiSort2() { + return gsiSort2; + } + + public void setGsiSort2(String gsiSort2) { + this.gsiSort2 = gsiSort2; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompositeKeyFakeItem that = (CompositeKeyFakeItem) o; + return Objects.equals(id, that.id) && + Objects.equals(gsiKey1, that.gsiKey1) && + Objects.equals(gsiKey2, that.gsiKey2) && + Objects.equals(gsiSort1, that.gsiSort1) && + Objects.equals(gsiSort2, that.gsiSort2); + } + + @Override + public int hashCode() { + return Objects.hash(id, gsiKey1, gsiKey2, gsiSort1, gsiSort2); + } + + public static class Builder { + private String id; + private String gsiKey1; + private String gsiKey2; + private String gsiSort1; + private String gsiSort2; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder gsiKey1(String gsiKey1) { + this.gsiKey1 = gsiKey1; + return this; + } + + public Builder gsiKey2(String gsiKey2) { + this.gsiKey2 = gsiKey2; + return this; + } + + public Builder gsiSort1(String gsiSort1) { + this.gsiSort1 = gsiSort1; + return this; + } + + public Builder gsiSort2(String gsiSort2) { + this.gsiSort2 = gsiSort2; + return this; + } + + public CompositeKeyFakeItem build() { + return new CompositeKeyFakeItem(this); + } + } + + public static final TableSchema SCHEMA = + StaticTableSchema.builder(CompositeKeyFakeItem.class) + .newItemSupplier(CompositeKeyFakeItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(CompositeKeyFakeItem::getId) + .setter(CompositeKeyFakeItem::setId) + .tags(StaticAttributeTags.primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsiKey1") + .getter(CompositeKeyFakeItem::getGsiKey1) + .setter(CompositeKeyFakeItem::setGsiKey1) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsiKey2") + .getter(CompositeKeyFakeItem::getGsiKey2) + .setter(CompositeKeyFakeItem::setGsiKey2) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.SECOND))) + .addAttribute(String.class, a -> a.name("gsiSort1") + .getter(CompositeKeyFakeItem::getGsiSort1) + .setter(CompositeKeyFakeItem::setGsiSort1) + .tags(StaticAttributeTags.secondarySortKey("gsi1", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsiSort2") + .getter(CompositeKeyFakeItem::getGsiSort2) + .setter(CompositeKeyFakeItem::setGsiSort2) + .tags(StaticAttributeTags.secondarySortKey("gsi1", Order.SECOND))) + .build(); +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithCompositeGsi.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithCompositeGsi.java new file mode 100644 index 000000000000..1e29955095f0 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithCompositeGsi.java @@ -0,0 +1,114 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primarySortKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; + +public class FakeItemWithCompositeGsi { + private static final StaticTableSchema TABLE_SCHEMA = + StaticTableSchema.builder(FakeItemWithCompositeGsi.class) + .newItemSupplier(FakeItemWithCompositeGsi::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeItemWithCompositeGsi::getId) + .setter(FakeItemWithCompositeGsi::setId) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("sort") + .getter(FakeItemWithCompositeGsi::getSort) + .setter(FakeItemWithCompositeGsi::setSort) + .tags(primarySortKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeItemWithCompositeGsi::getGsiPk1) + .setter(FakeItemWithCompositeGsi::setGsiPk1) + .tags(secondaryPartitionKey("composite_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeItemWithCompositeGsi::getGsiPk2) + .setter(FakeItemWithCompositeGsi::setGsiPk2) + .tags(secondaryPartitionKey("composite_gsi", Order.SECOND))) + .addAttribute(String.class, a -> a.name("gsi_sk1") + .getter(FakeItemWithCompositeGsi::getGsiSk1) + .setter(FakeItemWithCompositeGsi::setGsiSk1) + .tags(secondarySortKey("composite_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_sk2") + .getter(FakeItemWithCompositeGsi::getGsiSk2) + .setter(FakeItemWithCompositeGsi::setGsiSk2) + .tags(secondarySortKey("composite_gsi", Order.SECOND))) + .build(); + + private String id; + private String sort; + private String gsiPk1; + private String gsiPk2; + private String gsiSk1; + private String gsiSk2; + + public static StaticTableSchema getTableSchema() { + return TABLE_SCHEMA; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + public String getGsiPk1() { + return gsiPk1; + } + + public void setGsiPk1(String gsiPk1) { + this.gsiPk1 = gsiPk1; + } + + public String getGsiPk2() { + return gsiPk2; + } + + public void setGsiPk2(String gsiPk2) { + this.gsiPk2 = gsiPk2; + } + + public String getGsiSk1() { + return gsiSk1; + } + + public void setGsiSk1(String gsiSk1) { + this.gsiSk1 = gsiSk1; + } + + public String getGsiSk2() { + return gsiSk2; + } + + public void setGsiSk2(String gsiSk2) { + this.gsiSk2 = gsiSk2; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithFlattenedGsi.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithFlattenedGsi.java new file mode 100644 index 000000000000..9f44a9cb5bc5 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithFlattenedGsi.java @@ -0,0 +1,134 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class FakeItemWithFlattenedGsi { + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(FakeItemWithFlattenedGsi.class); + + public static TableSchema getTableSchema() { + return TABLE_SCHEMA; + } + + private String id; + private String sort; + private FlattenedGsiKeys gsiKeys; + private FlattenedGsiSortKeys gsiSortKeys; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbFlatten + public FlattenedGsiKeys getGsiKeys() { + return gsiKeys; + } + + public void setGsiKeys(FlattenedGsiKeys gsiKeys) { + this.gsiKeys = gsiKeys; + } + + @DynamoDbFlatten + public FlattenedGsiSortKeys getGsiSortKeys() { + return gsiSortKeys; + } + + public void setGsiSortKeys(FlattenedGsiSortKeys gsiSortKeys) { + this.gsiSortKeys = gsiSortKeys; + } + + @DynamoDbBean + public static class FlattenedGsiKeys { + private String gsiPartitionKey; + private String gsiMixedPartitionKey; + + @DynamoDbSecondaryPartitionKey(indexNames = "flatten_partition_gsi") + public String getGsiPartitionKey() { + return gsiPartitionKey; + } + + public void setGsiPartitionKey(String gsiPartitionKey) { + this.gsiPartitionKey = gsiPartitionKey; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "flatten_mixed_gsi") + public String getGsiMixedPartitionKey() { + return gsiMixedPartitionKey; + } + + public void setGsiMixedPartitionKey(String gsiMixedPartitionKey) { + this.gsiMixedPartitionKey = gsiMixedPartitionKey; + } + } + + @DynamoDbBean + public static class FlattenedGsiSortKeys { + private String gsiSortKey; + private String gsiMixedSortKey; + private String gsiBothSortKey; + + @DynamoDbSecondarySortKey(indexNames = "flatten_sort_gsi") + public String getGsiSortKey() { + return gsiSortKey; + } + + public void setGsiSortKey(String gsiSortKey) { + this.gsiSortKey = gsiSortKey; + } + + @DynamoDbSecondarySortKey(indexNames = "flatten_mixed_gsi") + public String getGsiMixedSortKey() { + return gsiMixedSortKey; + } + + public void setGsiMixedSortKey(String gsiMixedSortKey) { + this.gsiMixedSortKey = gsiMixedSortKey; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "flatten_both_gsi") + @DynamoDbSecondarySortKey(indexNames = "flatten_both_gsi") + public String getGsiBothSortKey() { + return gsiBothSortKey; + } + + public void setGsiBothSortKey(String gsiBothSortKey) { + this.gsiBothSortKey = gsiBothSortKey; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithMixedCompositeGsi.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithMixedCompositeGsi.java new file mode 100644 index 000000000000..7ccb45872e8d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/functionaltests/models/FakeItemWithMixedCompositeGsi.java @@ -0,0 +1,150 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.enhanced.dynamodb.functionaltests.models; + +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class FakeItemWithMixedCompositeGsi { + private static final TableSchema TABLE_SCHEMA = + TableSchema.fromClass(FakeItemWithMixedCompositeGsi.class); + + public static TableSchema getTableSchema() { + return TABLE_SCHEMA; + } + + private String id; + private String sort; + private String rootPartitionKey1; + private String rootPartitionKey2; + private String rootSortKey1; + private String rootSortKey2; + private FlattenedKeys flattenedKeys; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = Order.FIRST) + public String getRootPartitionKey1() { + return rootPartitionKey1; + } + + public void setRootPartitionKey1(String rootPartitionKey1) { + this.rootPartitionKey1 = rootPartitionKey1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi", "mixed_sort_gsi"}, order = Order.SECOND) + public String getRootPartitionKey2() { + return rootPartitionKey2; + } + + public void setRootPartitionKey2(String rootPartitionKey2) { + this.rootPartitionKey2 = rootPartitionKey2; + } + + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FIRST) + public String getRootSortKey1() { + return rootSortKey1; + } + + public void setRootSortKey1(String rootSortKey1) { + this.rootSortKey1 = rootSortKey1; + } + + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.SECOND) + public String getRootSortKey2() { + return rootSortKey2; + } + + public void setRootSortKey2(String rootSortKey2) { + this.rootSortKey2 = rootSortKey2; + } + + @DynamoDbFlatten + public FlattenedKeys getFlattenedKeys() { + return flattenedKeys; + } + + public void setFlattenedKeys(FlattenedKeys flattenedKeys) { + this.flattenedKeys = flattenedKeys; + } + + @DynamoDbBean + public static class FlattenedKeys { + private String flattenedPartitionKey1; + private String flattenedPartitionKey2; + private String flattenedSortKey1; + private String flattenedSortKey2; + + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.THIRD) + public String getFlattenedPartitionKey1() { + return flattenedPartitionKey1; + } + + public void setFlattenedPartitionKey1(String flattenedPartitionKey1) { + this.flattenedPartitionKey1 = flattenedPartitionKey1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = {"mixed_partition_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedPartitionKey2() { + return flattenedPartitionKey2; + } + + public void setFlattenedPartitionKey2(String flattenedPartitionKey2) { + this.flattenedPartitionKey2 = flattenedPartitionKey2; + } + + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.THIRD) + public String getFlattenedSortKey1() { + return flattenedSortKey1; + } + + public void setFlattenedSortKey1(String flattenedSortKey1) { + this.flattenedSortKey1 = flattenedSortKey1; + } + + @DynamoDbSecondarySortKey(indexNames = {"mixed_sort_gsi", "full_mixed_gsi"}, order = Order.FOURTH) + public String getFlattenedSortKey2() { + return flattenedSortKey2; + } + + public void setFlattenedSortKey2(String flattenedSortKey2) { + this.flattenedSortKey2 = flattenedSortKey2; + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalConsumerBuilderTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalConsumerBuilderTest.java new file mode 100644 index 000000000000..333aa43661b7 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalConsumerBuilderTest.java @@ -0,0 +1,557 @@ +/* + * 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.conditional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.CompositeKeyFakeItem; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class QueryConditionalConsumerBuilderTest { + + @Test + public void keyEqualTo_consumerBuilder_compositePartitionKeys() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(2)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + + assertThat(expression.expressionValues().size(), is(2)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + } + + @Test + public void keyEqualTo_consumerBuilder_compositeKeysWithSort() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(4)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort2", "gsiSort2")); + + assertThat(expression.expressionValues().size(), is(4)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort2", AttributeValue.builder().s("sort2").build())); + } + + @Test + public void sortGreaterThan_consumerBuilder_compositeKeys() { + Expression expression = QueryConditional.sortGreaterThan(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 > :AMZN_MAPPED_gsiSort2"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(4)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort2", "gsiSort2")); + + assertThat(expression.expressionValues().size(), is(4)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort2", AttributeValue.builder().s("sort2").build())); + } + + @Test + public void sortLessThan_consumerBuilder_singleSortKey() { + Expression expression = QueryConditional.sortLessThan(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 < :AMZN_MAPPED_gsiSort1"; + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(3)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + } + + @Test + public void sortGreaterThanOrEqualTo_consumerBuilder() { + Expression expression = QueryConditional.sortGreaterThanOrEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 >= :AMZN_MAPPED_gsiSort1"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(3)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + } + + @Test + public void sortLessThanOrEqualTo_consumerBuilder() { + Expression expression = QueryConditional.sortLessThanOrEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 <= :AMZN_MAPPED_gsiSort1"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(3)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + } + + @Test + public void sortBetween_consumerBuilder_compositeKeys() { + Expression expression = QueryConditional.sortBetween( + k1 -> k1.partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortA").build())), + k2 -> k2.partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortZ").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(4)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort2", "gsiSort2")); + + assertThat(expression.expressionValues().size(), is(5)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort2", AttributeValue.builder().s("sortA").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort22", + AttributeValue.builder().s("sortZ").build())); + } + + @Test + public void sortBeginsWith_consumerBuilder_compositeKeys() { + Expression expression = QueryConditional.sortBeginsWith(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("prefix").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "begins_with(#AMZN_MAPPED_gsiSort2, :AMZN_MAPPED_gsiSort2)"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(4)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort2", "gsiSort2")); + + assertThat(expression.expressionValues().size(), is(4)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort2", + AttributeValue.builder().s("prefix").build())); + } + + @Test + public void sortBeginsWith_consumerBuilder_singleSortKey() { + Expression expression = QueryConditional.sortBeginsWith(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("prefix").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(3)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", + AttributeValue.builder().s("prefix").build())); + } + + @Test + public void keyEqualTo_consumerBuilder_backwardCompatibility() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .partitionValue("singleKey")) + .expression(CompositeKeyFakeItem.SCHEMA, "$PRIMARY_INDEX"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_id = :AMZN_MAPPED_id")); + assertThat(expression.expressionNames().size(), is(1)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_id", "id")); + assertThat(expression.expressionValues().size(), is(1)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_id", AttributeValue.builder().s("singleKey").build())); + } + + @Test + public void keyEqualTo_consumerBuilder_mixedMethods() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValue("singleSort")) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(3)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", + AttributeValue.builder().s("singleSort").build())); + } + + @Test(expected = IllegalArgumentException.class) + public void keyEqualTo_consumerBuilder_incompletePartitionKeys() { + QueryConditional.keyEqualTo(k -> k + .partitionValues(Collections.singletonList( + AttributeValue.builder().s("key1").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void sortGreaterThan_consumerBuilder_noSortKeys() { + QueryConditional.sortGreaterThan(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void sortBeginsWith_consumerBuilder_nullSortValue() { + QueryConditional.sortBeginsWith(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().nul(true).build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void sortBeginsWith_consumerBuilder_numericSortValue() { + QueryConditional.sortBeginsWith(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().n("123").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test + public void keyEqualTo_consumerBuilder_partitionValuesFromStrings() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .addPartitionValue("key1") + .addPartitionValue("key2")) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"; + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(2)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + + assertThat(expression.expressionValues().size(), is(2)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + } + + @Test + public void sortGreaterThan_consumerBuilder_partitionValuesFromStrings() { + Expression expression = QueryConditional.sortGreaterThan(k -> k + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sort1") + .addSortValue("sort2")) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 > :AMZN_MAPPED_gsiSort2"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(4)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort2", "gsiSort2")); + + assertThat(expression.expressionValues().size(), is(4)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().s("sort1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort2", AttributeValue.builder().s("sort2").build())); + } + + @Test + public void keyEqualTo_consumerBuilder_partitionValuesFromNumbers() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .addPartitionValue(123) + .addPartitionValue(456)) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(2)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + + assertThat(expression.expressionValues().size(), is(2)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().n("123").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().n("456").build())); + } + + @Test + public void sortLessThan_consumerBuilder_partitionValuesFromNumbers() { + Expression expression = QueryConditional.sortLessThan(k -> k + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(789)) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 < :AMZN_MAPPED_gsiSort1"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(3)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().n("123").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().n("456").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().n("789").build())); + } + + @Test + public void keyEqualTo_consumerBuilder_partitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + + Expression expression = QueryConditional.keyEqualTo(k -> k + .addPartitionValue(bytes1) + .addPartitionValue(bytes2)) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(2)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + + assertThat(expression.expressionValues().size(), is(2)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().b(bytes1).build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().b(bytes2).build())); + } + + @Test + public void sortBetween_consumerBuilder_partitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + SdkBytes sortBytes1 = SdkBytes.fromUtf8String("sortA"); + SdkBytes sortBytes2 = SdkBytes.fromUtf8String("sortZ"); + + Expression expression = QueryConditional.sortBetween( + k1 -> k1.addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addSortValue(sortBytes1), + k2 -> k2.addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addSortValue(sortBytes2)) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 AND :AMZN_MAPPED_gsiSort12"; + + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(3)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + + assertThat(expression.expressionValues().size(), is(4)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().b(bytes1).build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().b(bytes2).build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", + AttributeValue.builder().b(sortBytes1).build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort12", + AttributeValue.builder().b(sortBytes2).build())); + } + + @Test + public void keyEqualTo_consumerBuilder_mixedTypes() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue(123) + .addSortValue(456)) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"; + assertThat(expression.expression(), is(expectedExpression)); + + assertThat(expression.expressionNames().size(), is(4)); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort1", "gsiSort1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiSort2", "gsiSort2")); + + assertThat(expression.expressionValues().size(), is(4)); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort1", AttributeValue.builder().n("123").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiSort2", AttributeValue.builder().n("456").build())); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalGsiCompositeKeysTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalGsiCompositeKeysTest.java new file mode 100644 index 000000000000..3ffda5b0c824 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalGsiCompositeKeysTest.java @@ -0,0 +1,592 @@ +/* + * 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.conditional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; + +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.CompositeKeyFakeItem; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class QueryConditionalGsiCompositeKeysTest { + + private static final TableSchema COMPOSITE_SCHEMA = CompositeKeyFakeItem.SCHEMA; + + @Test + public void equalToConditional_compositePartitionKeys_allProvided() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + assertThat(expression.expression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionValues(), + hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), + hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + } + + @Test + public void equalToConditional_compositeKeys_partitionAndSort() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalToConditional_partialSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build())) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test(expected = IllegalArgumentException.class) + public void equalToConditional_incompletePartitionKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Collections.singletonList( + AttributeValue.builder().s("key1").build())) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void equalToConditional_tooManySortKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build(), + AttributeValue.builder().s("sort3").build())) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test + public void singleKeyItemConditional_greaterThan_singleSortKey() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build())) + .build(); + + QueryConditional conditional = new SingleKeyItemConditional(key, ">"); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 > :AMZN_MAPPED_gsiSort1"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void singleKeyItemConditional_lessThanOrEqual_multipleSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + QueryConditional conditional = new SingleKeyItemConditional(key, "<="); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 <= :AMZN_MAPPED_gsiSort2"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test(expected = IllegalArgumentException.class) + public void singleKeyItemConditional_noSortKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional conditional = new SingleKeyItemConditional(key, ">"); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void singleKeyItemConditional_nullSortValue_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().nul(true).build())) + .build(); + + QueryConditional conditional = new SingleKeyItemConditional(key, ">"); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test + public void betweenConditional_singleSortKey() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortA").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortZ").build())) + .build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 AND :AMZN_MAPPED_gsiSort12"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void betweenConditional_multipleSortKeys() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortA").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortZ").build())) + .build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test(expected = IllegalArgumentException.class) + public void betweenConditional_noSortKeys_throwsException() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void betweenConditional_nullSortValue_throwsException() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().nul(true).build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortZ").build())) + .build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test + public void beginsWithConditional_singleSortKey() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("prefix").build())) + .build(); + + QueryConditional conditional = new BeginsWithConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void beginsWithConditional_multipleSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("prefix").build())) + .build(); + + QueryConditional conditional = new BeginsWithConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "begins_with(#AMZN_MAPPED_gsiSort2, :AMZN_MAPPED_gsiSort2)"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test(expected = IllegalArgumentException.class) + public void beginsWithConditional_noSortKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional conditional = new BeginsWithConditional(key); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void beginsWithConditional_nullSortValue_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().nul(true).build())) + .build(); + + QueryConditional conditional = new BeginsWithConditional(key); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void beginsWithConditional_numericSortValue_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().n("123").build())) + .build(); + + QueryConditional conditional = new BeginsWithConditional(key); + conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + } + + @Test + public void equalToConditional_backwardCompatibility_singleKey() { + Key singleKey = Key.builder() + .partitionValue("singlePartition") + .build(); + + QueryConditional conditional = new EqualToConditional(singleKey); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "$PRIMARY_INDEX"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_id = :AMZN_MAPPED_id")); + } + + @Test + public void queryConditional_keyEqualTo_compositeKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void queryConditional_sortGreaterThan_compositeKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 > :AMZN_MAPPED_gsiSort2"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void queryConditional_sortBetween_compositeKeys() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortA").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortZ").build())) + .build(); + + Expression expression = QueryConditional.sortBetween(key1, key2) + .expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 AND :AMZN_MAPPED_gsiSort12"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void queryConditional_sortBeginsWith_compositeKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("prefix").build())) + .build(); + + Expression expression = QueryConditional.sortBeginsWith(key) + .expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalToConditional_partitionValuesFromStrings() { + Key key = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + assertThat(expression.expression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + } + + @Test + public void singleKeyItemConditional_partitionValuesFromStrings() { + Key key = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sort1") + .addSortValue("sort2") + .build(); + + QueryConditional conditional = new SingleKeyItemConditional(key, ">="); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 >= :AMZN_MAPPED_gsiSort2"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalToConditional_partitionValuesFromNumbers() { + Key key = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + assertThat(expression.expression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + } + + @Test + public void betweenConditional_partitionValuesFromNumbers() { + Key key1 = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(100) + .build(); + + Key key2 = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(200) + .build(); + + QueryConditional conditional = new BetweenConditional(key1, key2); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 AND :AMZN_MAPPED_gsiSort12"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalToConditional_partitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + assertThat(expression.expression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + } + + @Test + public void beginsWithConditional_partitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + SdkBytes sortBytes = SdkBytes.fromUtf8String("prefix"); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addSortValue(sortBytes) + .build(); + + QueryConditional conditional = new BeginsWithConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalToConditional_mixedTypes() { + Key key = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue(123) + .addSortValue(456) + .build(); + + QueryConditional conditional = new EqualToConditional(key); + Expression expression = conditional.expression(COMPOSITE_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND " + + "#AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND " + + "#AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2"; + assertThat(expression.expression(), is(expectedExpression)); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalMaxKeysTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalMaxKeysTest.java new file mode 100644 index 000000000000..8275dfb4ca90 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalMaxKeysTest.java @@ -0,0 +1,586 @@ +/* + * 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.conditional; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.Arrays; +import org.junit.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.Expression; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +public class QueryConditionalMaxKeysTest { + + private static class MaxKeysItem { + private String id; + private String pk1; + private String pk2; + private String pk3; + private String pk4; + private String sk1; + private String sk2; + private String sk3; + private String sk4; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPk1() { + return pk1; + } + + public void setPk1(String pk1) { + this.pk1 = pk1; + } + + public String getPk2() { + return pk2; + } + + public void setPk2(String pk2) { + this.pk2 = pk2; + } + + public String getPk3() { + return pk3; + } + + public void setPk3(String pk3) { + this.pk3 = pk3; + } + + public String getPk4() { + return pk4; + } + + public void setPk4(String pk4) { + this.pk4 = pk4; + } + + public String getSk1() { + return sk1; + } + + public void setSk1(String sk1) { + this.sk1 = sk1; + } + + public String getSk2() { + return sk2; + } + + public void setSk2(String sk2) { + this.sk2 = sk2; + } + + public String getSk3() { + return sk3; + } + + public void setSk3(String sk3) { + this.sk3 = sk3; + } + + public String getSk4() { + return sk4; + } + + public void setSk4(String sk4) { + this.sk4 = sk4; + } + } + + private static final TableSchema MAX_KEYS_SCHEMA = + StaticTableSchema.builder(MaxKeysItem.class) + .newItemSupplier(MaxKeysItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(MaxKeysItem::getId) + .setter(MaxKeysItem::setId) + .tags(StaticAttributeTags.primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("pk1") + .getter(MaxKeysItem::getPk1) + .setter(MaxKeysItem::setPk1) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.FIRST))) + .addAttribute(String.class, a -> a.name("pk2") + .getter(MaxKeysItem::getPk2) + .setter(MaxKeysItem::setPk2) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.SECOND))) + .addAttribute(String.class, a -> a.name("pk3") + .getter(MaxKeysItem::getPk3) + .setter(MaxKeysItem::setPk3) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.THIRD))) + .addAttribute(String.class, a -> a.name("pk4") + .getter(MaxKeysItem::getPk4) + .setter(MaxKeysItem::setPk4) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.FOURTH))) + .addAttribute(String.class, a -> a.name("sk1") + .getter(MaxKeysItem::getSk1) + .setter(MaxKeysItem::setSk1) + .tags(StaticAttributeTags.secondarySortKey("gsi1", Order.FIRST))) + .addAttribute(String.class, a -> a.name("sk2") + .getter(MaxKeysItem::getSk2) + .setter(MaxKeysItem::setSk2) + .tags(StaticAttributeTags.secondarySortKey("gsi1", Order.SECOND))) + .addAttribute(String.class, a -> a.name("sk3") + .getter(MaxKeysItem::getSk3) + .setter(MaxKeysItem::setSk3) + .tags(StaticAttributeTags.secondarySortKey("gsi1", Order.THIRD))) + .addAttribute(String.class, a -> a.name("sk4") + .getter(MaxKeysItem::getSk4) + .setter(MaxKeysItem::setSk4) + .tags(StaticAttributeTags.secondarySortKey("gsi1", Order.FOURTH))) + .build(); + + @Test + public void equalTo_maxPartitionKeys_allProvided() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalTo_maxSortKeys_allProvided() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("sk4").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3 AND " + + "#AMZN_MAPPED_sk4 = :AMZN_MAPPED_sk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortGreaterThan_maxKeys_rightmostSortKey() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("sk4").build())) + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3 AND " + + "#AMZN_MAPPED_sk4 > :AMZN_MAPPED_sk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortBetween_maxKeys() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("skA").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("skZ").build())) + .build(); + + Expression expression = QueryConditional.sortBetween(key1, key2) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3 AND " + + "#AMZN_MAPPED_sk4 BETWEEN :AMZN_MAPPED_sk4 AND :AMZN_MAPPED_sk42"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortBeginsWith_maxKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("prefix").build())) + .build(); + + Expression expression = QueryConditional.sortBeginsWith(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3 AND " + + "begins_with(#AMZN_MAPPED_sk4, :AMZN_MAPPED_sk4)"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalTo_partialSortKeys_firstThree() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortGreaterThan_partialSortKeys_secondKey() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build())) + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 > :AMZN_MAPPED_sk2"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test(expected = IllegalArgumentException.class) + public void equalTo_incompletePartitionKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build())) + .build(); + + QueryConditional.keyEqualTo(key).expression(MAX_KEYS_SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void equalTo_tooManySortKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().s("sk2").build(), + AttributeValue.builder().s("sk3").build(), + AttributeValue.builder().s("sk4").build(), + AttributeValue.builder().s("sk5").build())) + .build(); + + QueryConditional.keyEqualTo(key).expression(MAX_KEYS_SCHEMA, "gsi1"); + } + + @Test + public void equalTo_mixedDataTypes() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("stringKey").build(), + AttributeValue.builder().n("123").build(), + AttributeValue.builder().s("anotherString").build(), + AttributeValue.builder().n("456").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sortString").build(), + AttributeValue.builder().n("789").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test(expected = IllegalArgumentException.class) + public void equalTo_nullInMiddleOfSortKeys_throwsException() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("pk1").build(), + AttributeValue.builder().s("pk2").build(), + AttributeValue.builder().s("pk3").build(), + AttributeValue.builder().s("pk4").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sk1").build(), + AttributeValue.builder().nul(true).build(), + AttributeValue.builder().s("sk3").build())) + .build(); + + QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + } + + @Test + public void equalTo_addMaxPartitionKeysFromStrings() { + Key key = Key.builder() + .addPartitionValue("pk1") + .addPartitionValue("pk2") + .addPartitionValue("pk3") + .addPartitionValue("pk4") + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortGreaterThan_addMaxKeysFromStrings() { + Key key = Key.builder() + .addPartitionValue("pk1") + .addPartitionValue("pk2") + .addPartitionValue("pk3") + .addPartitionValue("pk4") + .addSortValue("sk1") + .addSortValue("sk2") + .addSortValue("sk3") + .addSortValue("sk4") + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3 AND " + + "#AMZN_MAPPED_sk4 > :AMZN_MAPPED_sk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalTo_addMaxPartitionKeysFromNumbers() { + Key key = Key.builder() + .addPartitionValue(1) + .addPartitionValue(2) + .addPartitionValue(3) + .addPartitionValue(4) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortLessThan_addMaxKeysFromNumbers() { + Key key = Key.builder() + .addPartitionValue(1) + .addPartitionValue(2) + .addPartitionValue(3) + .addPartitionValue(4) + .addSortValue(10) + .addSortValue(20) + .addSortValue(30) + .addSortValue(40) + .build(); + + Expression expression = QueryConditional.sortLessThan(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 = :AMZN_MAPPED_sk1 AND " + + "#AMZN_MAPPED_sk2 = :AMZN_MAPPED_sk2 AND " + + "#AMZN_MAPPED_sk3 = :AMZN_MAPPED_sk3 AND " + + "#AMZN_MAPPED_sk4 < :AMZN_MAPPED_sk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void equalTo_addMaxPartitionKeysFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + SdkBytes bytes3 = SdkBytes.fromUtf8String("binary3"); + SdkBytes bytes4 = SdkBytes.fromUtf8String("binary4"); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addPartitionValue(bytes3) + .addPartitionValue(bytes4) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4"; + assertThat(expression.expression(), is(expectedExpression)); + } + + @Test + public void sortBetween_maxKeysFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + SdkBytes bytes3 = SdkBytes.fromUtf8String("binary3"); + SdkBytes bytes4 = SdkBytes.fromUtf8String("binary4"); + SdkBytes sortBytesA = SdkBytes.fromUtf8String("sortA"); + SdkBytes sortBytesZ = SdkBytes.fromUtf8String("sortZ"); + + Key key1 = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addPartitionValue(bytes3) + .addPartitionValue(bytes4) + .addSortValue(sortBytesA) + .build(); + + Key key2 = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addPartitionValue(bytes3) + .addPartitionValue(bytes4) + .addSortValue(sortBytesZ) + .build(); + + Expression expression = QueryConditional.sortBetween(key1, key2) + .expression(MAX_KEYS_SCHEMA, "gsi1"); + + String expectedExpression = "#AMZN_MAPPED_pk1 = :AMZN_MAPPED_pk1 AND " + + "#AMZN_MAPPED_pk2 = :AMZN_MAPPED_pk2 AND " + + "#AMZN_MAPPED_pk3 = :AMZN_MAPPED_pk3 AND " + + "#AMZN_MAPPED_pk4 = :AMZN_MAPPED_pk4 AND " + + "#AMZN_MAPPED_sk1 BETWEEN :AMZN_MAPPED_sk1 AND :AMZN_MAPPED_sk12"; + assertThat(expression.expression(), is(expectedExpression)); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalUtilsTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalUtilsTest.java new file mode 100644 index 000000000000..dff9a11ad77d --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/conditional/QueryConditionalUtilsTest.java @@ -0,0 +1,87 @@ +/* + * 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.conditional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; +import software.amazon.awssdk.enhanced.dynamodb.internal.conditional.QueryConditionalUtils.KeyResolution; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +class QueryConditionalUtilsTest { + + private static final TableSchema SIMPLE_SCHEMA = FakeItem.getTableSchema(); + + @Test + void resolveKeys_singlePartitionKey_returnsCorrectResolution() { + Key key = Key.builder().partitionValue("pk1").build(); + + KeyResolution resolution = QueryConditionalUtils.resolveKeys(key, SIMPLE_SCHEMA, "$PRIMARY_INDEX"); + + assertThat(resolution.partitionKeys).containsExactly("id"); + assertThat(resolution.partitionValues).containsExactly(AttributeValue.builder().s("pk1").build()); + assertThat(resolution.sortKeys).isEmpty(); + assertThat(resolution.sortValues).isEmpty(); + } + + @Test + void keyResolution_constructor_validatesInputs() { + List partitionKeys = Collections.singletonList("pk1"); + List partitionValues = Collections.singletonList(AttributeValue.builder().s("val1").build()); + List sortKeys = Collections.singletonList("sk1"); + List sortValues = Collections.singletonList(AttributeValue.builder().s("val2").build()); + + KeyResolution resolution = new KeyResolution(partitionKeys, partitionValues, sortKeys, sortValues); + + assertThat(resolution.partitionKeys).isEqualTo(partitionKeys); + assertThat(resolution.partitionValues).isEqualTo(partitionValues); + assertThat(resolution.sortKeys).isEqualTo(sortKeys); + assertThat(resolution.sortValues).isEqualTo(sortValues); + } + + @Test + void keyResolution_hasSortKeys_returnsTrueWhenBothPresent() { + KeyResolution resolution = new KeyResolution( + Collections.singletonList("pk1"), + Collections.singletonList(AttributeValue.builder().s("val1").build()), + Collections.singletonList("sk1"), + Collections.singletonList(AttributeValue.builder().s("val2").build()) + ); + + assertThat(resolution.hasSortKeys()).isTrue(); + } + + @Test + void keyResolution_fieldsAreImmutable() { + List partitionKeys = Collections.singletonList("pk1"); + List partitionValues = Collections.singletonList(AttributeValue.builder().s("val1").build()); + + KeyResolution resolution = new KeyResolution(partitionKeys, partitionValues, Collections.emptyList(), + Collections.emptyList()); + + assertThatThrownBy(() -> resolution.partitionKeys.add("newKey")) + .isInstanceOf(UnsupportedOperationException.class); + + assertThatThrownBy(() -> resolution.partitionValues.add(AttributeValue.builder().s("newVal").build())) + .isInstanceOf(UnsupportedOperationException.class); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java index 5347119b26dc..b058314279c7 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/CreateTableOperationTest.java @@ -31,6 +31,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import org.junit.Test; @@ -43,8 +45,15 @@ import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithBinaryKey; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithByteBufferKey; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithCompositeGsi; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithFlattenedGsi; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithIndices; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithMixedCompositeGsi; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithNumericSort; +import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeMetadataImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CrossIndexImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedFlattenedImmutable; import software.amazon.awssdk.enhanced.dynamodb.model.CreateTableEnhancedRequest; import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedGlobalSecondaryIndex; import software.amazon.awssdk.enhanced.dynamodb.model.EnhancedLocalSecondaryIndex; @@ -53,6 +62,7 @@ import software.amazon.awssdk.services.dynamodb.model.BillingMode; 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.KeySchemaElement; import software.amazon.awssdk.services.dynamodb.model.Projection; import software.amazon.awssdk.services.dynamodb.model.ProjectionType; @@ -473,4 +483,397 @@ public void transformResults_doesNothing() { operation.transformResponse(response, FakeItem.getTableSchema(), PRIMARY_CONTEXT, null); } + + @Test + public void generateRequest_gsiWithSingleKeys_buildsCorrectly() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi_1") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithIndices.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("gsi_1")); + assertThat(gsi.keySchema().size(), is(2)); + } + + @Test + public void generateRequest_gsiWithCompositeKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("composite_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(5L).writeCapacityUnits(5L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithCompositeGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + + assertThat(gsi.indexName(), is("composite_gsi")); + assertThat(gsi.keySchema().size(), is(4)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("gsi_pk1", "gsi_pk2")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("gsi_sk1", "gsi_sk2")); + } + + @Test + public void generateRequest_gsiWithFlattenedPartitionKey() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("flatten_partition_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("flatten_partition_gsi")); + assertThat(gsi.keySchema().size(), is(1)); + assertThat(gsi.keySchema().get(0).attributeName(), is("gsiPartitionKey")); + assertThat(gsi.keySchema().get(0).keyType(), is(HASH)); + } + + @Test + public void generateRequest_gsiWithFlattenedSortKey() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("flatten_sort_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("flatten_sort_gsi")); + assertThat(gsi.keySchema().size(), is(2)); + assertThat(gsi.keySchema().get(0).attributeName(), is("id")); + assertThat(gsi.keySchema().get(0).keyType(), is(HASH)); + assertThat(gsi.keySchema().get(1).attributeName(), is("gsiSortKey")); + assertThat(gsi.keySchema().get(1).keyType(), is(RANGE)); + } + + @Test + public void generateRequest_gsiWithMixedFlattenedKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("flatten_mixed_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("flatten_mixed_gsi")); + assertThat(gsi.keySchema().size(), is(2)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("gsiMixedPartitionKey")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("gsiMixedSortKey")); + } + + @Test + public void generateRequest_gsiWithBothFlattenedKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("flatten_both_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithFlattenedGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("flatten_both_gsi")); + assertThat(gsi.keySchema().size(), is(2)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("gsiBothSortKey")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("gsiBothSortKey")); + } + + @Test + public void generateRequest_gsiWithMixedCompositePartitionKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("mixed_partition_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithMixedCompositeGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("mixed_partition_gsi")); + assertThat(gsi.keySchema().size(), is(4)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("rootPartitionKey1", "rootPartitionKey2", "flattenedPartitionKey1", "flattenedPartitionKey2")); + } + + @Test + public void generateRequest_gsiWithMixedCompositeSortKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("mixed_sort_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithMixedCompositeGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("mixed_sort_gsi")); + assertThat(gsi.keySchema().size(), is(6)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("rootPartitionKey1", "rootPartitionKey2")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("rootSortKey1", "rootSortKey2", "flattenedSortKey1", "flattenedSortKey2")); + } + + @Test + public void generateRequest_gsiWithFullMixedCompositeKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("full_mixed_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(FakeItemWithMixedCompositeGsi.getTableSchema(), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("full_mixed_gsi")); + assertThat(gsi.keySchema().size(), is(8)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("rootPartitionKey1", "rootPartitionKey2", "flattenedPartitionKey1", "flattenedPartitionKey2")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("rootSortKey1", "rootSortKey2", "flattenedSortKey1", "flattenedSortKey2")); + } + + @Test + public void generateRequest_immutableGsiWithCompositeKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi1") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(5L).writeCapacityUnits(5L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(ImmutableTableSchema.create(CompositeMetadataImmutable.class), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("gsi1")); + assertThat(gsi.keySchema().size(), is(4)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("gsiPk1", "gsiPk2")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("gsiSk1", "gsiSk2")); + } + + @Test + public void generateRequest_immutableGsiWithCrossIndexKeys() { + List gsiList = Arrays.asList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi1") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build(), + EnhancedGlobalSecondaryIndex.builder() + .indexName("gsi2") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(ImmutableTableSchema.create(CrossIndexImmutable.class), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(2)); + + GlobalSecondaryIndex gsi1 = request.globalSecondaryIndexes().stream() + .filter(gsi -> "gsi1".equals(gsi.indexName())) + .findFirst().orElse(null); + assertThat(gsi1.keySchema().size(), is(2)); + assertThat(gsi1.keySchema().get(0).attributeName(), is("attr1")); + assertThat(gsi1.keySchema().get(0).keyType(), is(HASH)); + assertThat(gsi1.keySchema().get(1).attributeName(), is("attr2")); + assertThat(gsi1.keySchema().get(1).keyType(), is(HASH)); + + GlobalSecondaryIndex gsi2 = request.globalSecondaryIndexes().stream() + .filter(gsi -> "gsi2".equals(gsi.indexName())) + .findFirst().orElse(null); + assertThat(gsi2.keySchema().size(), is(2)); + assertThat(gsi2.keySchema().get(0).attributeName(), is("attr3")); + assertThat(gsi2.keySchema().get(0).keyType(), is(HASH)); + assertThat(gsi2.keySchema().get(1).attributeName(), is("attr1")); + assertThat(gsi2.keySchema().get(1).keyType(), is(RANGE)); + } + + @Test + public void generateRequest_immutableGsiWithMixedFlattenedKeys() { + List gsiList = Collections.singletonList( + EnhancedGlobalSecondaryIndex.builder() + .indexName("mixed_gsi") + .projection(p -> p.projectionType(ProjectionType.ALL)) + .provisionedThroughput(p -> p.readCapacityUnits(1L).writeCapacityUnits(1L)) + .build()); + + CreateTableOperation operation = + CreateTableOperation.create(CreateTableEnhancedRequest.builder() + .globalSecondaryIndices(gsiList) + .build()); + + CreateTableRequest request = operation.generateRequest(ImmutableTableSchema.create(MixedFlattenedImmutable.class), + PRIMARY_CONTEXT, null); + + assertThat(request.globalSecondaryIndexes().size(), is(1)); + GlobalSecondaryIndex gsi = request.globalSecondaryIndexes().get(0); + assertThat(gsi.indexName(), is("mixed_gsi")); + assertThat(gsi.keySchema().size(), is(4)); + + Set partitionKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == HASH) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(partitionKeyNames, containsInAnyOrder("rootKey1", "flatKey1")); + + Set sortKeyNames = gsi.keySchema().stream() + .filter(key -> key.keyType() == RANGE) + .map(KeySchemaElement::attributeName) + .collect(Collectors.toSet()); + assertThat(sortKeyNames, containsInAnyOrder("rootKey2", "flatKey2")); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationConditionalTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationConditionalTest.java index 9e2483ae65d9..2a0faab41add 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationConditionalTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationConditionalTest.java @@ -22,11 +22,19 @@ import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithSort; import static software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort.createUniqueFakeItemWithoutSort; +import java.util.Arrays; +import java.util.Collections; import java.util.UUID; import org.junit.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags; +import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.CompositeKeyFakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithNumericSort; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort; @@ -179,7 +187,7 @@ public void beginsWith_hashAndRangeKey_bothSet() { Expression expression = QueryConditional.sortBeginsWith(getKey(fakeItemWithSort)) .expression(FakeItemWithSort.getTableSchema(), TableMetadata.primaryIndexName()); - String expectedExpression = String.format("%s = %s AND begins_with ( %s, %s )", ID_KEY, ID_VALUE, SORT_KEY, + String expectedExpression = String.format("%s = %s AND begins_with(%s, %s)", ID_KEY, ID_VALUE, SORT_KEY, SORT_VALUE); assertThat(expression.expression(), is(expectedExpression)); assertThat(expression.expressionValues(), hasEntry(ID_VALUE, fakeItemWithSortHashValue)); @@ -203,7 +211,8 @@ public void beginsWith_hashAndSort_onlyHashSet_throwsIllegalArgumentException() @Test(expected = IllegalArgumentException.class) public void beginsWith_numericRange_throwsIllegalArgumentException() { FakeItemWithNumericSort fakeItemWithNumericSort = FakeItemWithNumericSort.createUniqueFakeItemWithSort(); - QueryConditional.sortBeginsWith(getKey(fakeItemWithNumericSort)).expression(FakeItemWithNumericSort.getTableSchema(), TableMetadata.primaryIndexName()); + QueryConditional.sortBeginsWith(getKey(fakeItemWithNumericSort)).expression(FakeItemWithNumericSort.getTableSchema(), + TableMetadata.primaryIndexName()); } @Test @@ -214,7 +223,7 @@ public void between_allKeysSet_stringSort() { AttributeValue.builder().s(otherFakeItemWithSort.getSort()).build(); Expression expression = QueryConditional.sortBetween(getKey(fakeItemWithSort), getKey(otherFakeItemWithSort)) - .expression(FakeItemWithSort.getTableSchema(), TableMetadata.primaryIndexName()); + .expression(FakeItemWithSort.getTableSchema(), TableMetadata.primaryIndexName()); String expectedExpression = String.format("%s = %s AND %s BETWEEN %s AND %s", ID_KEY, ID_VALUE, SORT_KEY, SORT_VALUE, SORT_OTHER_VALUE); @@ -253,7 +262,7 @@ private void verifyExpression(Expression expression, String condition) { assertThat(expression.expressionValues(), hasEntry(ID_VALUE, fakeItemWithSortHashValue)); assertThat(expression.expressionValues(), hasEntry(SORT_VALUE, fakeItemWithSortSortValue)); } - + private Key getKey(FakeItem item) { return EnhancedClientUtils.createKeyFromItem(item, FakeItem.getTableSchema(), TableMetadata.primaryIndexName()); } @@ -267,4 +276,568 @@ private Key getKey(FakeItemWithNumericSort item) { FakeItemWithNumericSort.getTableSchema(), TableMetadata.primaryIndexName()); } -} + + @Test + public void equalTo_gsiCompositePartitionKeys_allProvided() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey1", "gsiKey1")); + assertThat(expression.expressionNames(), hasEntry("#AMZN_MAPPED_gsiKey2", "gsiKey2")); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("key1").build())); + assertThat(expression.expressionValues(), hasEntry(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("key2").build())); + } + + @Test + public void equalTo_gsiCompositeKeys_partitionAndSort() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void equalTo_gsiCompositeKeys_partialSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + } + + @Test(expected = IllegalArgumentException.class) + public void equalTo_gsiCompositeKeys_incompletePartitionKeys() { + Key key = Key.builder() + .partitionValues(Collections.singletonList( + AttributeValue.builder().s("key1").build())) + .build(); + + QueryConditional.keyEqualTo(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void equalTo_gsiCompositeKeys_tooManySortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build(), + AttributeValue.builder().s("sort3").build())) + .build(); + + QueryConditional.keyEqualTo(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test + public void sortGreaterThan_gsiCompositeKeys_singleSortKey() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build())) + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 > :AMZN_MAPPED_gsiSort1")); + } + + @Test + public void sortGreaterThan_gsiCompositeKeys_multipleSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 > :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void sortLessThanOrEqualTo_gsiCompositeKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + Expression expression = QueryConditional.sortLessThanOrEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 <= :AMZN_MAPPED_gsiSort2")); + } + + @Test(expected = IllegalArgumentException.class) + public void sortGreaterThan_gsiCompositeKeys_noSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional.sortGreaterThan(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test + public void sortBetween_gsiCompositeKeys_singleSortKey() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortA").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortZ").build())) + .build(); + + Expression expression = QueryConditional.sortBetween(key1, key2) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 " + + "AND :AMZN_MAPPED_gsiSort12")); + } + + @Test + public void sortBetween_gsiCompositeKeys_multipleSortKeys() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortA").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortZ").build())) + .build(); + + Expression expression = QueryConditional.sortBetween(key1, key2) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND " + + ":AMZN_MAPPED_gsiSort22")); + } + + @Test(expected = IllegalArgumentException.class) + public void sortBetween_gsiCompositeKeys_noSortKeys() { + Key key1 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + Key key2 = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional.sortBetween(key1, key2).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test + public void sortBeginsWith_gsiCompositeKeys_singleSortKey() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("prefix").build())) + .build(); + + Expression expression = QueryConditional.sortBeginsWith(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND begins_with(#AMZN_MAPPED_gsiSort1, " + + ":AMZN_MAPPED_gsiSort1)")); + } + + @Test + public void sortBeginsWith_gsiCompositeKeys_multipleSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("prefix").build())) + .build(); + + Expression expression = QueryConditional.sortBeginsWith(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "begins_with(#AMZN_MAPPED_gsiSort2, :AMZN_MAPPED_gsiSort2)")); + } + + @Test(expected = IllegalArgumentException.class) + public void sortBeginsWith_gsiCompositeKeys_noSortKeys() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryConditional.sortBeginsWith(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test + public void equalTo_backwardCompatibility_singleKey() { + Expression expression = QueryConditional.keyEqualTo(getKey(fakeItem)) + .expression(FakeItem.getTableSchema(), TableMetadata.primaryIndexName()); + + assertThat(expression.expression(), is(ID_KEY + " = " + ID_VALUE)); + assertThat(expression.expressionNames(), hasEntry(ID_KEY, "id")); + assertThat(expression.expressionValues(), hasEntry(ID_VALUE, fakeItemHashValue)); + } + + @Test + public void sortGreaterThan_backwardCompatibility_singleKey() { + Expression expression = QueryConditional.sortGreaterThan(getKey(fakeItemWithSort)) + .expression(FakeItemWithSort.getTableSchema(), TableMetadata.primaryIndexName()); + + verifyExpression(expression, ">"); + } + + @Test + public void keyEqualTo_consumerBuilder_gsiCompositeKeys() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + } + + @Test + public void sortBetween_consumerBuilder_gsiCompositeKeys() { + Expression expression = QueryConditional.sortBetween( + k1 -> k1.partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortA").build())), + k2 -> k2.partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sortZ").build()))) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 " + + "AND :AMZN_MAPPED_gsiSort12")); + } + + @Test(expected = IllegalArgumentException.class) + public void equalTo_gsiCompositeKeys_nullPartitionValue() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().nul(true).build())) + .build(); + + QueryConditional.keyEqualTo(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void sortBeginsWith_gsiCompositeKeys_nullSortValue() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().nul(true).build())) + .build(); + + QueryConditional.sortBeginsWith(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test(expected = IllegalArgumentException.class) + public void sortBeginsWith_gsiCompositeKeys_numericSortValue() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().n("123").build())) + .build(); + + QueryConditional.sortBeginsWith(key).expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + } + + @Test + public void equalTo_gsiCompositeKeys_maxPartitionKeys() { + TableSchema maxKeysSchema = + StaticTableSchema.builder(CompositeKeyFakeItem.class) + .newItemSupplier(CompositeKeyFakeItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(CompositeKeyFakeItem::getId) + .setter(CompositeKeyFakeItem::setId) + .tags(StaticAttributeTags.primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsiKey1") + .getter(CompositeKeyFakeItem::getGsiKey1) + .setter(CompositeKeyFakeItem::setGsiKey1) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsiKey2") + .getter(CompositeKeyFakeItem::getGsiKey2) + .setter(CompositeKeyFakeItem::setGsiKey2) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.SECOND))) + .addAttribute(String.class, a -> a.name("gsiSort1") + .getter(CompositeKeyFakeItem::getGsiSort1) + .setter(CompositeKeyFakeItem::setGsiSort1) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.THIRD))) + .addAttribute(String.class, a -> a.name("gsiSort2") + .getter(CompositeKeyFakeItem::getGsiSort2) + .setter(CompositeKeyFakeItem::setGsiSort2) + .tags(StaticAttributeTags.secondaryPartitionKey("gsi1", Order.FOURTH))) + .build(); + + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build(), + AttributeValue.builder().s("key3").build(), + AttributeValue.builder().s("key4").build())) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(maxKeysSchema, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void equalTo_gsiCompositeKeys_addPartitionValuesFromStrings() { + Key key = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2")); + } + + @Test + public void sortGreaterThan_gsiCompositeKeys_addPartitionValuesFromStrings() { + Key key = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sort1") + .addSortValue("sort2") + .build(); + + Expression expression = QueryConditional.sortGreaterThan(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 > :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void sortBetween_gsiCompositeKeys_addPartitionValuesFromStrings() { + Key key1 = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sortA") + .build(); + + Key key2 = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sortZ") + .build(); + + Expression expression = QueryConditional.sortBetween(key1, key2) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 " + + "AND :AMZN_MAPPED_gsiSort12")); + } + + @Test + public void equalTo_gsiCompositeKeys_addPartitionValuesFromNumbers() { + Key key = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2")); + } + + @Test + public void sortLessThan_gsiCompositeKeys_addPartitionValuesFromNumbers() { + Key key = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(789) + .addSortValue(101112) + .build(); + + Expression expression = QueryConditional.sortLessThan(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 < :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void equalTo_gsiCompositeKeys_addPartitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2")); + } + + @Test + public void sortBeginsWith_gsiCompositeKeys_addPartitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + SdkBytes sortBytes = SdkBytes.fromUtf8String("prefix"); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addSortValue(sortBytes) + .build(); + + Expression expression = QueryConditional.sortBeginsWith(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND begins_with(#AMZN_MAPPED_gsiSort1, " + + ":AMZN_MAPPED_gsiSort1)")); + } + + @Test + public void equalTo_gsiCompositeKeys_mixedTypes() { + Key key = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue(123) + .addSortValue(456) + .build(); + + Expression expression = QueryConditional.keyEqualTo(key) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND " + + "#AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void keyEqualTo_consumerBuilder_addPartitionValuesFromStrings() { + Expression expression = QueryConditional.keyEqualTo(k -> k + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sort1")) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + } + + @Test + public void sortBetween_consumerBuilder_addPartitionValuesFromNumbers() { + Expression expression = QueryConditional.sortBetween( + k1 -> k1.addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(100), + k2 -> k2.addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(200)) + .expression(CompositeKeyFakeItem.SCHEMA, "gsi1"); + + assertThat(expression.expression(), is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = " + + ":AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 BETWEEN :AMZN_MAPPED_gsiSort1 " + + "AND :AMZN_MAPPED_gsiSort12")); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java index fba0cb2328fa..cb67cf7877b5 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/internal/operations/QueryOperationTest.java @@ -32,12 +32,14 @@ import static software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional.keyEqualTo; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.IntStream; +import software.amazon.awssdk.core.SdkBytes; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; @@ -48,10 +50,12 @@ import software.amazon.awssdk.core.pagination.sync.SdkIterable; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClientExtension; import software.amazon.awssdk.enhanced.dynamodb.DynamoDbExtensionContext; +import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.Expression; import software.amazon.awssdk.enhanced.dynamodb.OperationContext; import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.extensions.ReadModification; +import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.CompositeKeyFakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItem; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithIndices; import software.amazon.awssdk.enhanced.dynamodb.functionaltests.models.FakeItemWithSort; @@ -409,7 +413,7 @@ public void queryItem_withExtension_correctlyTransformsItem() { modifiedResultItems.stream() .map(QueryOperationTest::getAttributeValueMap) .map(attributeMap -> ReadModification.builder().transformedItem(attributeMap).build()) - .collect(Collectors.toList()) + .collect(toList()) .toArray(new ReadModification[]{}); when(mockDynamoDbEnhancedClientExtension.afterRead(any(DynamoDbExtensionContext.AfterRead.class))) @@ -445,10 +449,462 @@ private static QueryResponse generateFakeQueryResults(List generateFakeItemList() { - return IntStream.range(0, 3).mapToObj(ignored -> FakeItem.createUniqueFakeItem()).collect(toList()); + return IntStream.range(0, 3).mapToObj(ignored -> createUniqueFakeItem()).collect(toList()); } private static Map getAttributeValueMap(FakeItem fakeItem) { return singletonMap("id", AttributeValue.builder().s(fakeItem.getId()).build()); } -} + + private static final OperationContext GSI_COMPOSITE_CONTEXT = + DefaultOperationContext.create(TABLE_NAME, "gsi1"); + + @Test + public void generateRequest_gsiCompositeKeys_partitionKeysOnly() { + Key compositeKey = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("gsiKey1").build(), + AttributeValue.builder().s("gsiKey2").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + Map names = new HashMap<>(); + names.put("#AMZN_MAPPED_gsiKey1", "gsiKey1"); + names.put("#AMZN_MAPPED_gsiKey2", "gsiKey2"); + + Map values = new HashMap<>(); + values.put(":AMZN_MAPPED_gsiKey1", AttributeValue.builder().s("gsiKey1").build()); + values.put(":AMZN_MAPPED_gsiKey2", AttributeValue.builder().s("gsiKey2").build()); + + QueryRequest expectedQueryRequest = QueryRequest.builder() + .tableName(TABLE_NAME) + .indexName("gsi1") + .keyConditionExpression("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2") + .expressionAttributeNames(names) + .expressionAttributeValues(values) + .build(); + + assertThat(queryRequest, is(expectedQueryRequest)); + } + + @Test + public void generateRequest_gsiCompositeKeys_partitionAndSortKeys() { + Key compositeKey = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + assertThat(queryRequest.indexName(), is("gsi1")); + } + + @Test + public void generateRequest_gsiCompositeKeys_sortBetween() { + Key keyFrom = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortA").build())) + .build(); + + Key keyTo = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sortZ").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(QueryConditional.sortBetween(keyFrom, keyTo)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22")); + } + + @Test + public void generateRequest_gsiCompositeKeys_sortBeginsWith() { + Key key = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("prefix").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(QueryConditional.sortBeginsWith(key)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND begins_with(#AMZN_MAPPED_gsiSort2, :AMZN_MAPPED_gsiSort2)")); + } + + @Test + public void generateRequest_gsiCompositeKeys_exclusiveStartKey() { + CompositeKeyFakeItem exclusiveStartKey = CompositeKeyFakeItem.builder() + .id("id1") + .gsiKey1("gsiKey1") + .gsiKey2("gsiKey2") + .gsiSort1("gsiSort1") + .gsiSort2("gsiSort2") + .build(); + + Set keyFields = new HashSet<>(CompositeKeyFakeItem.SCHEMA.tableMetadata().primaryKeys()); + keyFields.addAll(CompositeKeyFakeItem.SCHEMA.tableMetadata().indexKeys("gsi1")); + + Key compositeKey = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .exclusiveStartKey(CompositeKeyFakeItem.SCHEMA.itemToMap(exclusiveStartKey, keyFields)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.exclusiveStartKey(), + hasEntry("id", AttributeValue.builder().s("id1").build())); + assertThat(queryRequest.exclusiveStartKey(), + hasEntry("gsiKey1", AttributeValue.builder().s("gsiKey1").build())); + assertThat(queryRequest.exclusiveStartKey(), + hasEntry("gsiKey2", AttributeValue.builder().s("gsiKey2").build())); + assertThat(queryRequest.exclusiveStartKey(), + hasEntry("gsiSort1", AttributeValue.builder().s("gsiSort1").build())); + assertThat(queryRequest.exclusiveStartKey(), + hasEntry("gsiSort2", AttributeValue.builder().s("gsiSort2").build())); + } + + @Test + public void transformResults_gsiCompositeKeys_multipleItems() { + List queryResultItems = generateCompositeKeyFakeItemList(); + List> queryResultMaps = + queryResultItems.stream().map(QueryOperationTest::getCompositeKeyAttributeValueMap).collect(toList()); + + QueryResponse queryResponse = generateFakeQueryResults(queryResultMaps); + + QueryOperation queryOperation = QueryOperation.create( + QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .build())) + .build()); + + Page queryResultPage = queryOperation.transformResponse(queryResponse, + CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryResultPage.items(), is(queryResultItems)); + } + + @Test + public void generateRequest_gsiCompositeKeys_backwardCompatibility() { + FakeItemWithIndices fakeItem = createUniqueFakeItemWithIndices(); + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k.partitionValue(fakeItem.getGsiId()))) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(FakeItemWithIndices.getTableSchema(), GSI_1_CONTEXT, null); + + assertThat(queryRequest.indexName(), is("gsi_1")); + assertThat(queryRequest.keyConditionExpression(), is("#AMZN_MAPPED_gsi_id = :AMZN_MAPPED_gsi_id")); + } + + @Test + public void generateRequest_gsiCompositeKeys_consumerBuilder() { + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Collections.singletonList( + AttributeValue.builder().s("sort1").build())))) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + } + + @Test(expected = IllegalArgumentException.class) + public void generateRequest_gsiCompositeKeys_incompletePartitionKeys() { + Key incompleteKey = Key.builder() + .partitionValues(Collections.singletonList( + AttributeValue.builder().s("key1").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(incompleteKey)) + .build()); + + queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + } + + @Test(expected = IllegalArgumentException.class) + public void generateRequest_gsiCompositeKeys_tooManySortKeys() { + Key invalidKey = Key.builder() + .partitionValues(Arrays.asList( + AttributeValue.builder().s("key1").build(), + AttributeValue.builder().s("key2").build())) + .sortValues(Arrays.asList( + AttributeValue.builder().s("sort1").build(), + AttributeValue.builder().s("sort2").build(), + AttributeValue.builder().s("sort3").build())) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(invalidKey)) + .build()); + + queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + } + + private static List generateCompositeKeyFakeItemList() { + return IntStream.range(0, 3).mapToObj(i -> + CompositeKeyFakeItem.builder() + .id("id" + i) + .gsiKey1("gsiKey1_" + i) + .gsiKey2("gsiKey2_" + i) + .gsiSort1("gsiSort1_" + i) + .gsiSort2("gsiSort2_" + i) + .build() + ).collect(toList()); + } + + private static Map getCompositeKeyAttributeValueMap(CompositeKeyFakeItem item) { + Map map = new HashMap<>(); + map.put("id", AttributeValue.builder().s(item.getId()).build()); + map.put("gsiKey1", AttributeValue.builder().s(item.getGsiKey1()).build()); + map.put("gsiKey2", AttributeValue.builder().s(item.getGsiKey2()).build()); + map.put("gsiSort1", AttributeValue.builder().s(item.getGsiSort1()).build()); + map.put("gsiSort2", AttributeValue.builder().s(item.getGsiSort2()).build()); + return map; + } + + @Test + public void generateRequest_gsiCompositeKeys_partitionValuesFromStrings() { + Key compositeKey = Key.builder() + .addPartitionValue("gsiKey1") + .addPartitionValue("gsiKey2") + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + assertThat(queryRequest.indexName(), is("gsi1")); + } + + @Test + public void generateRequest_gsiCompositeKeys_partitionAndSortFromStrings() { + Key compositeKey = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sort1") + .addSortValue("sort2") + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + } + + @Test + public void generateRequest_gsiCompositeKeys_partitionValuesFromNumbers() { + Key compositeKey = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").n(), is("123")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").n(), is("456")); + } + + @Test + public void generateRequest_gsiCompositeKeys_sortBetweenFromNumbers() { + Key keyFrom = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(100) + .addSortValue(200) + .build(); + + Key keyTo = Key.builder() + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(100) + .addSortValue(300) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(QueryConditional.sortBetween(keyFrom, keyTo)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 BETWEEN :AMZN_MAPPED_gsiSort2 AND :AMZN_MAPPED_gsiSort22")); + } + + @Test + public void generateRequest_gsiCompositeKeys_partitionValuesFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + + Key compositeKey = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").b(), is(bytes1)); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").b(), is(bytes2)); + } + + @Test + public void generateRequest_gsiCompositeKeys_sortBeginsWithFromBinary() { + SdkBytes bytes1 = SdkBytes.fromUtf8String("binary1"); + SdkBytes bytes2 = SdkBytes.fromUtf8String("binary2"); + SdkBytes sortBytes = SdkBytes.fromUtf8String("prefix"); + + Key key = Key.builder() + .addPartitionValue(bytes1) + .addPartitionValue(bytes2) + .addSortValue(sortBytes) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(QueryConditional.sortBeginsWith(key)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND begins_with(#AMZN_MAPPED_gsiSort1, :AMZN_MAPPED_gsiSort1)")); + } + + @Test + public void generateRequest_gsiCompositeKeys_mixedTypes() { + Key compositeKey = Key.builder() + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue(123) + .addSortValue(456) + .build(); + + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(compositeKey)) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1 AND #AMZN_MAPPED_gsiSort2 = :AMZN_MAPPED_gsiSort2")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey1").s(), is("key1")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiKey2").s(), is("key2")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiSort1").n(), is("123")); + assertThat(queryRequest.expressionAttributeValues().get(":AMZN_MAPPED_gsiSort2").n(), is("456")); + } + + @Test + public void generateRequest_gsiCompositeKeys_consumerBuilderFromStrings() { + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k + .addPartitionValue("key1") + .addPartitionValue("key2") + .addSortValue("sort1"))) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + } + + @Test + public void generateRequest_gsiCompositeKeys_consumerBuilderFromNumbers() { + QueryOperation queryToTest = + QueryOperation.create(QueryEnhancedRequest.builder() + .queryConditional(keyEqualTo(k -> k + .addPartitionValue(123) + .addPartitionValue(456) + .addSortValue(789))) + .build()); + + QueryRequest queryRequest = queryToTest.generateRequest(CompositeKeyFakeItem.SCHEMA, GSI_COMPOSITE_CONTEXT, null); + + assertThat(queryRequest.keyConditionExpression(), + is("#AMZN_MAPPED_gsiKey1 = :AMZN_MAPPED_gsiKey1 AND #AMZN_MAPPED_gsiKey2 = :AMZN_MAPPED_gsiKey2 AND #AMZN_MAPPED_gsiSort1 = :AMZN_MAPPED_gsiSort1")); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java index 0b2ab9276909..238a63129869 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/BeanTableSchemaTest.java @@ -23,6 +23,8 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.binaryValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.numberValue; @@ -35,6 +37,8 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; +import java.util.List; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -42,7 +46,7 @@ import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.enhanced.dynamodb.EnhancedType; -import software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues; +import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; @@ -52,6 +56,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AttributeConverterNoConstructorBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CommonTypesBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DocumentBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DuplicateOrderBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EmptyConverterProvidersInvalidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EmptyConverterProvidersValidBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.EnumBean; @@ -61,6 +66,7 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FluentSetterBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.IgnoredAttributeBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.InvalidBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeKeyMaxBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ListBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MapBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MultipleConverterProvidersBean; @@ -77,6 +83,11 @@ import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SimpleBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SingleConverterProvidersBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.SortKeyBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.TwoPartitionKeyBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ThreeSortKeyBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedCompositeBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NonSequentialOrderBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedOrderingBean; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; @RunWith(MockitoJUnitRunner.class) @@ -85,6 +96,11 @@ public class BeanTableSchemaTest { @Rule public ExpectedException exception = ExpectedException.none(); + @After + public void tearDown() { + BeanTableSchema.clearSchemaCache(); + } + @Test public void simpleBean_correctlyAssignsPrimaryPartitionKey() { BeanTableSchema beanTableSchema = BeanTableSchema.create(SimpleBean.class); @@ -543,7 +559,7 @@ public void itemToMap_nullAttribute_ignoreNullsFalse() { assertThat(itemMap.size(), is(2)); assertThat(itemMap, hasEntry("id", stringValue("id-value"))); - assertThat(itemMap, hasEntry("integerAttribute", AttributeValues.nullAttributeValue())); + assertThat(itemMap, hasEntry("integerAttribute", nullAttributeValue())); } @Test @@ -931,7 +947,7 @@ public void mapBean_mapWithNullValue() { Map itemMap = beanTableSchema.itemToMap(mapBean, true); Map expectedMap = new HashMap<>(); - expectedMap.put("one", AttributeValues.nullAttributeValue()); + expectedMap.put("one", nullAttributeValue()); expectedMap.put("three", stringValue("four")); AttributeValue expectedMapValue = AttributeValue.builder() .m(expectedMap) @@ -1205,6 +1221,87 @@ public void create_withNonMapFlatten_succeeds() { BeanTableSchema.create(NonMapFlattenBean.class); } + @Test + public void compositeKeyMaxBean_fourPartitionAndFourSortKeys_correctOrder() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(CompositeKeyMaxBean.class); + + List partitionKeys = beanTableSchema.tableMetadata().indexPartitionKeys("gsi1"); + assertThat(partitionKeys, hasSize(4)); + assertThat(partitionKeys.get(0), is("gsiPk1")); + assertThat(partitionKeys.get(1), is("gsiPk2")); + assertThat(partitionKeys.get(2), is("gsiPk3")); + assertThat(partitionKeys.get(3), is("gsiPk4")); + + List sortKeys = beanTableSchema.tableMetadata().indexSortKeys("gsi1"); + assertThat(sortKeys, hasSize(4)); + assertThat(sortKeys.get(0), is("gsiSk1")); + assertThat(sortKeys.get(1), is("gsiSk2")); + assertThat(sortKeys.get(2), is("gsiSk3")); + assertThat(sortKeys.get(3), is("gsiSk4")); + } + + @Test + public void compositeKeyBean_twoPartitionKeys_correctOrder() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(TwoPartitionKeyBean.class); + + List partitionKeys = beanTableSchema.tableMetadata().indexPartitionKeys("gsi1"); + assertThat(partitionKeys, hasSize(2)); + assertThat(partitionKeys, contains("key2", "key1")); + } + + @Test + public void compositeKeyBean_threeSortKeys_correctOrder() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(ThreeSortKeyBean.class); + + List sortKeys = beanTableSchema.tableMetadata().indexSortKeys("gsi1"); + assertThat(sortKeys, hasSize(3)); + assertThat(sortKeys, contains("sort2", "sort3", "sort1")); + } + + @Test + public void compositeKeyBean_mixedComposite_twoPartitionThreeSort() { + BeanTableSchema beanTableSchema = BeanTableSchema.create(MixedCompositeBean.class); + + List partitionKeys = beanTableSchema.tableMetadata().indexPartitionKeys("gsi1"); + assertThat(partitionKeys, hasSize(2)); + assertThat(partitionKeys, contains("pk1", "pk2")); + + List sortKeys = beanTableSchema.tableMetadata().indexSortKeys("gsi1"); + assertThat(sortKeys, hasSize(3)); + assertThat(sortKeys, contains("sk2", "sk1", "sk3")); + } + + @Test + public void compositeKeyBean_duplicateOrderValues_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Duplicate partition key order 0 for index 'gsi1'"); + BeanTableSchema.create(DuplicateOrderBean.class); + } + + @Test + public void compositeKeyBean_nonSequentialOrders_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Non-sequential partition key orders for index 'gsi1'. Expected: 0,1,2,3 but got: [0, 2]"); + BeanTableSchema.create(NonSequentialOrderBean.class); + } + + @Test + public void compositeKeyBean_mixedExplicitImplicitOrdering_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Composite partition keys for index 'gsi1' must all have explicit ordering (0,1,2,3)"); + BeanTableSchema.create(MixedOrderingBean.class); + } + + @Test + public void rootSchema_areCached_but_flattenedAreNot() { + BeanTableSchema root1 = BeanTableSchema.create(CompositeKeyMaxBean.class, ExecutionContext.ROOT); + BeanTableSchema root2 = BeanTableSchema.create(CompositeKeyMaxBean.class, ExecutionContext.ROOT); + assertThat(root1, is(root2)); + + BeanTableSchema flattened = BeanTableSchema.create(CompositeKeyMaxBean.class, ExecutionContext.FLATTENED); + assertThat(root1, not(flattened)); + } + @DynamoDbBean public static class MultipleFlattenMapsBean { private String id; diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java index 3e97f4d4b660..6ecf3fedb454 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/ImmutableTableSchemaTest.java @@ -17,26 +17,53 @@ import static java.util.Collections.singletonMap; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; -import org.junit.jupiter.api.Test; +import java.util.Optional; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.ExecutionContext; +import software.amazon.awssdk.enhanced.dynamodb.IndexMetadata; +import software.amazon.awssdk.enhanced.dynamodb.KeyAttributeMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.AbstractImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CompositeMetadataImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.CrossIndexImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DocumentImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.DuplicateOrderImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedBeanImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.FlattenedImmutableImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.MixedOrderingImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NestedImmutableIgnoreNulls; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.NonSequentialOrderImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.OrderPreservationImmutable; import software.amazon.awssdk.enhanced.dynamodb.mapper.testbeans.ToBuilderImmutable; +import software.amazon.awssdk.enhanced.dynamodb.model.ImmutableCompositeKeyRecord; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; public class ImmutableTableSchemaTest { + @Rule + public ExpectedException exception = ExpectedException.none(); + + @After + public void tearDown() { + ImmutableTableSchema.clearSchemaCache(); + } + @Test public void documentImmutable_correctlyMapsBeanAttributes() { ImmutableTableSchema documentImmutableTableSchema = @@ -298,4 +325,101 @@ public void toBuilderImmutable_ignoresToBuilderMethod() { assertThat(itemMap, hasEntry("id", stringValue("id-value"))); assertThat(itemMap, hasEntry("attribute1", stringValue("one"))); } + + @Test + public void fromImmutable_constructsTableMetadata_withGSICompositeKeys() { + ImmutableTableSchema schema = ImmutableTableSchema.create(CompositeMetadataImmutable.class); + TableMetadata metadata = schema.tableMetadata(); + + assertThat(metadata.indexPartitionKey(TableMetadata.primaryIndexName()), is("id")); + assertThat(metadata.indexSortKey(TableMetadata.primaryIndexName()), is(Optional.of("sort"))); + + List gsiPartitionKeys = metadata.indexPartitionKeys("gsi1"); + assertThat(gsiPartitionKeys, contains("gsiPk1", "gsiPk2")); + + List gsiSortKeys = metadata.indexSortKeys("gsi1"); + assertThat(gsiSortKeys, contains("gsiSk1", "gsiSk2")); + } + + @Test + public void fromImmutable_constructsTableMetadata_withGSICompositePartitionKeys_AndOrderPreserved() { + ImmutableTableSchema schema = ImmutableTableSchema.create(OrderPreservationImmutable.class); + TableMetadata metadata = schema.tableMetadata(); + + Optional gsi1Metadata = metadata.indices().stream() + .filter(index -> "gsi1".equals(index.name())) + .findFirst(); + + assertThat(gsi1Metadata.isPresent(), is(true)); + + List partitionKeysMetadata = gsi1Metadata.get().partitionKeys(); + assertThat(partitionKeysMetadata.size(), is(4)); + + assertThat(partitionKeysMetadata.get(0).name(), is("key3")); + assertThat(partitionKeysMetadata.get(0).order().getIndex(), is(0)); + assertThat(partitionKeysMetadata.get(0).attributeValueType(), is(AttributeValueType.S)); + + assertThat(partitionKeysMetadata.get(1).name(), is("key2")); + assertThat(partitionKeysMetadata.get(1).order().getIndex(), is(1)); + assertThat(partitionKeysMetadata.get(1).attributeValueType(), is(AttributeValueType.S)); + + assertThat(partitionKeysMetadata.get(2).name(), is("key4")); + assertThat(partitionKeysMetadata.get(2).order().getIndex(), is(2)); + assertThat(partitionKeysMetadata.get(2).attributeValueType(), is(AttributeValueType.S)); + + assertThat(partitionKeysMetadata.get(3).name(), is("key1")); + assertThat(partitionKeysMetadata.get(3).order().getIndex(), is(3)); + assertThat(partitionKeysMetadata.get(3).attributeValueType(), is(AttributeValueType.S)); + } + + @Test + public void fromImmutable_constructsTableMetadata_withGSICompositeKeys_crossIndexConsistency() { + ImmutableTableSchema schema = ImmutableTableSchema.create(CrossIndexImmutable.class); + + List gsi1PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi1"); + assertThat(gsi1PartitionKeys.size(), is(2)); + assertThat(gsi1PartitionKeys, contains("attr1", "attr2")); + + List gsi2PartitionKeys = schema.tableMetadata().indexPartitionKeys("gsi2"); + assertThat(gsi2PartitionKeys.size(), is(1)); + assertThat(gsi2PartitionKeys, contains("attr3")); + + List gsi2SortKeys = schema.tableMetadata().indexSortKeys("gsi2"); + assertThat(gsi2SortKeys.size(), is(1)); + assertThat(gsi2SortKeys, contains("attr1")); + } + + @Test + public void compositeKeyImmutable_duplicateOrderValues_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Duplicate partition key order 0 for index 'gsi1'"); + ImmutableTableSchema.create(DuplicateOrderImmutable.class); + } + + @Test + public void compositeKeyImmutable_nonSequentialOrders_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Non-sequential partition key orders for index 'gsi1'. Expected: 0,1,2,3 but got: [0, 2]"); + ImmutableTableSchema.create(NonSequentialOrderImmutable.class); + } + + @Test + public void compositeKeyImmutable_mixedExplicitImplicitOrdering_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Composite partition keys for index 'gsi1' must all have explicit ordering (0,1,2,3)"); + ImmutableTableSchema.create(MixedOrderingImmutable.class); + } + + @Test + public void rootSchema_areCached_but_flattenedAreNot() { + ImmutableTableSchema root1 = ImmutableTableSchema.create(ImmutableCompositeKeyRecord.class + , ExecutionContext.ROOT); + ImmutableTableSchema root2 = ImmutableTableSchema.create(ImmutableCompositeKeyRecord.class + , ExecutionContext.ROOT); + assertThat(root1, is(root2)); + + ImmutableTableSchema flattened = + ImmutableTableSchema.create(ImmutableCompositeKeyRecord.class, ExecutionContext.FLATTENED); + assertThat(root1, not(flattened)); + } } diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadataTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadataTest.java index c91c610065ec..d57784493c69 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadataTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableMetadataTest.java @@ -15,14 +15,17 @@ package software.amazon.awssdk.enhanced.dynamodb.mapper; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static software.amazon.awssdk.enhanced.dynamodb.TableMetadata.primaryIndexName; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import org.junit.Rule; @@ -190,6 +193,172 @@ public void getPrimaryKeys_unset() { tableMetadata.primaryKeys(); } + @Test + public void singleKeyImplicitOrdering() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.UNSPECIFIED); + builder.addIndexSortKey("gsi1", "sort1", AttributeValueType.S, Order.UNSPECIFIED); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys("gsi1"), contains("key1")); + assertThat(metadata.indexSortKeys("gsi1"), contains("sort1")); + } + + @Test + public void singleKeyExplicitOrdering() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.FIRST); + builder.addIndexSortKey("gsi1", "sort1", AttributeValueType.S, Order.FIRST); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys("gsi1"), contains("key1")); + assertThat(metadata.indexSortKeys("gsi1"), contains("sort1")); + } + + @Test + public void compositeKeysAllExplicit() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.FIRST); + builder.addIndexPartitionKey("gsi1", "key2", AttributeValueType.S, Order.SECOND); + builder.addIndexSortKey("gsi1", "sort1", AttributeValueType.S, Order.FIRST); + builder.addIndexSortKey("gsi1", "sort2", AttributeValueType.S, Order.SECOND); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys("gsi1"), contains("key1", "key2")); + assertThat(metadata.indexSortKeys("gsi1"), contains("sort1", "sort2")); + } + + @Test + public void separatePartitionAndSort() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "pk1", AttributeValueType.S, Order.FIRST); + builder.addIndexPartitionKey("gsi1", "pk2", AttributeValueType.S, Order.SECOND); + + builder.addIndexSortKey("gsi1", "sk1", AttributeValueType.S, Order.FIRST); + builder.addIndexSortKey("gsi1", "sk2", AttributeValueType.S, Order.SECOND); + builder.addIndexSortKey("gsi1", "sk3", AttributeValueType.S, Order.THIRD); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys("gsi1"), contains("pk1", "pk2")); + assertThat(metadata.indexSortKeys("gsi1"), contains("sk1", "sk2", "sk3")); + } + + @Test + public void multipleIndicesIndependent() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.FIRST); + builder.addIndexPartitionKey("gsi1", "key2", AttributeValueType.S, Order.SECOND); + + builder.addIndexPartitionKey("gsi2", "single_key", AttributeValueType.S, Order.UNSPECIFIED); + + builder.addIndexPartitionKey("gsi3", "keyA", AttributeValueType.S, Order.FIRST); + builder.addIndexPartitionKey("gsi3", "keyB", AttributeValueType.S, Order.SECOND); + builder.addIndexPartitionKey("gsi3", "keyC", AttributeValueType.S, Order.THIRD); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys("gsi1"), contains("key1", "key2")); + assertThat(metadata.indexPartitionKeys("gsi2"), contains("single_key")); + assertThat(metadata.indexPartitionKeys("gsi3"), contains("keyA", "keyB", "keyC")); + } + + @Test + public void primaryIndexSkipped() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey(primaryIndexName(), "id", AttributeValueType.S, Order.UNSPECIFIED); + builder.addIndexSortKey(primaryIndexName(), "sort", AttributeValueType.S, Order.UNSPECIFIED); + + builder.addIndexPartitionKey("gsi1", "gsi_key", AttributeValueType.S, Order.UNSPECIFIED); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys(primaryIndexName()), contains("id")); + assertThat(metadata.indexSortKeys(primaryIndexName()), contains("sort")); + assertThat(metadata.indexPartitionKeys("gsi1"), contains("gsi_key")); + } + + @Test + public void emptyIndex_throwsException() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.UNSPECIFIED); + + StaticTableMetadata metadata = builder.build(); + + assertThatThrownBy(() -> metadata.indexPartitionKeys("empty_index")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Attempt to execute an operation that requires a secondary index without defining the index " + + "attributes in the table metadata."); + } + + @Test + public void maxFourKeys_partitionKeys() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.FIRST); + builder.addIndexPartitionKey("gsi1", "key2", AttributeValueType.S, Order.SECOND); + builder.addIndexPartitionKey("gsi1", "key3", AttributeValueType.S, Order.THIRD); + builder.addIndexPartitionKey("gsi1", "key4", AttributeValueType.S, Order.FOURTH); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexPartitionKeys("gsi1"), hasSize(4)); + } + + @Test + public void maxFourKeys_sortKeys() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "pk", AttributeValueType.S, Order.UNSPECIFIED); + builder.addIndexSortKey("gsi1", "sort1", AttributeValueType.S, Order.FIRST); + builder.addIndexSortKey("gsi1", "sort2", AttributeValueType.S, Order.SECOND); + builder.addIndexSortKey("gsi1", "sort3", AttributeValueType.S, Order.THIRD); + builder.addIndexSortKey("gsi1", "sort4", AttributeValueType.S, Order.FOURTH); + + StaticTableMetadata metadata = builder.build(); + + assertThat(metadata.indexSortKeys("gsi1"), hasSize(4)); + } + + @Test + public void orderingPreservation() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key3", AttributeValueType.S, Order.THIRD); + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.FIRST); + builder.addIndexPartitionKey("gsi1", "key2", AttributeValueType.S, Order.SECOND); + + StaticTableMetadata metadata = builder.build(); + + List partitionKeys = metadata.indexPartitionKeys("gsi1"); + + assertThat(partitionKeys, hasSize(3)); + assertThat(partitionKeys, contains("key1", "key2", "key3")); + } + + @Test + public void builderReuse_independentValidation() { + StaticTableMetadata.Builder builder = StaticTableMetadata.builder(); + + builder.addIndexPartitionKey("gsi1", "key1", AttributeValueType.S, Order.FIRST); + StaticTableMetadata metadata1 = builder.build(); + assertThat(metadata1.indexPartitionKeys("gsi1"), contains("key1")); + + builder.addIndexPartitionKey("gsi1", "key2", AttributeValueType.S, Order.SECOND); + StaticTableMetadata metadata2 = builder.build(); + assertThat(metadata2.indexPartitionKeys("gsi1"), contains("key1", "key2")); + } + @Test public void getIndexKeys_partitionAndSort() { TableMetadata tableMetadata = StaticTableMetadata.builder() @@ -367,10 +536,10 @@ public void mergeWithDuplicateIndexPartitionKey() { StaticTableMetadata.Builder builder = StaticTableMetadata.builder().addIndexPartitionKey(INDEX_NAME, "id", AttributeValueType.S); exception.expect(IllegalArgumentException.class); - exception.expectMessage("partition key"); + exception.expectMessage("key"); exception.expectMessage(INDEX_NAME); - builder.mergeWith(builder.build()); + builder.mergeWith(builder.build()).build(); } @Test @@ -378,10 +547,10 @@ public void mergeWithDuplicateIndexSortKey() { StaticTableMetadata.Builder builder = StaticTableMetadata.builder().addIndexSortKey(INDEX_NAME, "id", AttributeValueType.S); exception.expect(IllegalArgumentException.class); - exception.expectMessage("sort key"); + exception.expectMessage("key"); exception.expectMessage(INDEX_NAME); - builder.mergeWith(builder.build()); + builder.mergeWith(builder.build()).build(); } @Test diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java index 368ef26b9648..24d2feef7e65 100644 --- a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/StaticTableSchemaTest.java @@ -21,6 +21,7 @@ import static java.util.Collections.singletonMap; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; @@ -29,6 +30,9 @@ import static org.mockito.Mockito.when; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.nullAttributeValue; import static software.amazon.awssdk.enhanced.dynamodb.internal.AttributeValues.stringValue; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.primaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondaryPartitionKey; +import static software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags.secondarySortKey; import java.math.BigDecimal; import java.util.Arrays; @@ -1560,5 +1564,206 @@ private void verifyNullableAttribute(EnhancedType attributeType, verifyAttribute(attributeType, staticAttribute, fakeMappedItem, attributeValue); verifyNullAttribute(attributeType, staticAttribute, FakeMappedItem.builder().build()); } -} + @Test + public void duplicatePartitionKeyOrder_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Duplicate partition key order 0 for index 'test_gsi'"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.FIRST))) + .build(); + } + + @Test + public void duplicateSortKeyOrder_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Duplicate sort key order 1 for index 'test_gsi'"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_sk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.SECOND))) + .addAttribute(String.class, a -> a.name("gsi_sk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.SECOND))) + .build(); + } + + @Test + public void mixOrderedAndUnorderedPartitionKeys_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Composite partition keys for index 'test_gsi' must all have explicit ordering (0,1,2,3)"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi"))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.SECOND))) + .build(); + } + + @Test + public void nonSequentialPartitionKeyOrders_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Non-sequential partition key orders for index 'test_gsi'. Expected: 0,1,2,3 but got: [0, 2]"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.THIRD))) + .build(); + } + + @Test + public void nonSequentialSortKeyOrders_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Non-sequential sort key orders for index 'test_gsi'. Expected: 0,1,2,3 but got: [0, 3]"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_sk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_sk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.FOURTH))) + .build(); + } + + @Test + public void invalidCompositeKeysWithImplicitOrder_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Composite partition keys for index 'test_gsi' must all have explicit ordering (0,1,2,3)"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi"))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi"))) + .build(); + } + + @Test + public void invalidNonCompositeKeyWithExplicitPartitionKeyOrder_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid non-composite partition key order for index 'test_gsi'. Expected: -1,0 but got: 1"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.SECOND))) + .build(); + } + + @Test + public void invalidNonCompositeKeyWithExplicitSortKeyOrder_throwsException() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid non-composite sort key order for index 'test_gsi'. Expected: -1,0 but got: 2"); + + StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi"))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.THIRD))) + .build(); + } + + @Test + public void validCompositeKeysWithExplicitOrder_succeeds() { + StaticTableSchema schema = StaticTableSchema.builder(FakeMappedItem.class) + .newItemSupplier(FakeMappedItem::new) + .addAttribute(String.class, a -> a.name("id") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(primaryPartitionKey())) + .addAttribute(String.class, a -> a.name("gsi_pk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_pk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondaryPartitionKey("test_gsi", Order.SECOND))) + .addAttribute(String.class, a -> a.name("gsi_sk1") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.FIRST))) + .addAttribute(String.class, a -> a.name("gsi_sk2") + .getter(FakeMappedItem::getAString) + .setter(FakeMappedItem::setAString) + .tags(secondarySortKey("test_gsi", Order.SECOND))) + .build(); + + assertThat(schema.tableMetadata().indexPartitionKeys("test_gsi"), contains("gsi_pk1", "gsi_pk2")); + assertThat(schema.tableMetadata().indexSortKeys("test_gsi"), contains("gsi_sk1", "gsi_sk2")); + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeKeyMaxBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeKeyMaxBean.java new file mode 100644 index 000000000000..3ff066e02190 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeKeyMaxBean.java @@ -0,0 +1,116 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbBean +public class CompositeKeyMaxBean { + private String id; + private String gsiPk1; + private String gsiPk2; + private String gsiPk3; + private String gsiPk4; + private String gsiSk1; + private String gsiSk2; + private String gsiSk3; + private String gsiSk4; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getGsiPk1() { + return gsiPk1; + } + + public void setGsiPk1(String gsiPk1) { + this.gsiPk1 = gsiPk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getGsiPk2() { + return gsiPk2; + } + + public void setGsiPk2(String gsiPk2) { + this.gsiPk2 = gsiPk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.THIRD) + public String getGsiPk3() { + return gsiPk3; + } + + public void setGsiPk3(String gsiPk3) { + this.gsiPk3 = gsiPk3; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FOURTH) + public String getGsiPk4() { + return gsiPk4; + } + + public void setGsiPk4(String gsiPk4) { + this.gsiPk4 = gsiPk4; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FIRST) + public String getGsiSk1() { + return gsiSk1; + } + + public void setGsiSk1(String gsiSk1) { + this.gsiSk1 = gsiSk1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.SECOND) + public String getGsiSk2() { + return gsiSk2; + } + + public void setGsiSk2(String gsiSk2) { + this.gsiSk2 = gsiSk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.THIRD) + public String getGsiSk3() { + return gsiSk3; + } + + public void setGsiSk3(String gsiSk3) { + this.gsiSk3 = gsiSk3; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FOURTH) + public String getGsiSk4() { + return gsiSk4; + } + + public void setGsiSk4(String gsiSk4) { + this.gsiSk4 = gsiSk4; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeMetadataBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeMetadataBean.java new file mode 100644 index 000000000000..65b22e4a2807 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeMetadataBean.java @@ -0,0 +1,87 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class CompositeMetadataBean { + private String id; + private String sort; + private String gsiPk1; + private String gsiPk2; + private String gsiSk1; + private String gsiSk2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getGsiPk1() { + return gsiPk1; + } + + public void setGsiPk1(String gsiPk1) { + this.gsiPk1 = gsiPk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getGsiPk2() { + return gsiPk2; + } + + public void setGsiPk2(String gsiPk2) { + this.gsiPk2 = gsiPk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FIRST) + public String getGsiSk1() { + return gsiSk1; + } + + public void setGsiSk1(String gsiSk1) { + this.gsiSk1 = gsiSk1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.SECOND) + public String getGsiSk2() { + return gsiSk2; + } + + public void setGsiSk2(String gsiSk2) { + this.gsiSk2 = gsiSk2; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeMetadataImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeMetadataImmutable.java new file mode 100644 index 000000000000..3fbec5483e95 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CompositeMetadataImmutable.java @@ -0,0 +1,145 @@ +/* + * 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.mapper.testbeans; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbImmutable(builder = CompositeMetadataImmutable.Builder.class) +public class CompositeMetadataImmutable { + private final String id; + private final String sort; + private final String gsiPk1; + private final String gsiPk2; + private final String gsiSk1; + private final String gsiSk2; + + private CompositeMetadataImmutable(Builder b) { + this.id = b.id; + this.sort = b.sort; + this.gsiPk1 = b.gsiPk1; + this.gsiPk2 = b.gsiPk2; + this.gsiSk1 = b.gsiSk1; + this.gsiSk2 = b.gsiSk2; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSortKey + public String sort() { + return sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String gsiPk1() { + return gsiPk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String gsiPk2() { + return gsiPk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FIRST) + public String gsiSk1() { + return gsiSk1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.SECOND) + public String gsiSk2() { + return gsiSk2; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompositeMetadataImmutable that = (CompositeMetadataImmutable) o; + return Objects.equals(id, that.id) && + Objects.equals(sort, that.sort) && + Objects.equals(gsiPk1, that.gsiPk1) && + Objects.equals(gsiPk2, that.gsiPk2) && + Objects.equals(gsiSk1, that.gsiSk1) && + Objects.equals(gsiSk2, that.gsiSk2); + } + + @Override + public int hashCode() { + return Objects.hash(id, sort, gsiPk1, gsiPk2, gsiSk1, gsiSk2); + } + + public static final class Builder { + private String id; + private String sort; + private String gsiPk1; + private String gsiPk2; + private String gsiSk1; + private String gsiSk2; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder sort(String sort) { + this.sort = sort; + return this; + } + + public Builder gsiPk1(String gsiPk1) { + this.gsiPk1 = gsiPk1; + return this; + } + + public Builder gsiPk2(String gsiPk2) { + this.gsiPk2 = gsiPk2; + return this; + } + + public Builder gsiSk1(String gsiSk1) { + this.gsiSk1 = gsiSk1; + return this; + } + + public Builder gsiSk2(String gsiSk2) { + this.gsiSk2 = gsiSk2; + return this; + } + + public CompositeMetadataImmutable build() { + return new CompositeMetadataImmutable(this); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CrossIndexBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CrossIndexBean.java new file mode 100644 index 000000000000..2d49ee1d0274 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CrossIndexBean.java @@ -0,0 +1,67 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbBean +public class CrossIndexBean { + private String id; + private String attr1; + private String attr2; + private String attr3; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + @DynamoDbSecondarySortKey(indexNames = "gsi2", order = Order.FIRST) + public String getAttr1() { + return attr1; + } + + public void setAttr1(String attr1) { + this.attr1 = attr1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getAttr2() { + return attr2; + } + + public void setAttr2(String attr2) { + this.attr2 = attr2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi2", order = Order.FIRST) + public String getAttr3() { + return attr3; + } + + public void setAttr3(String attr3) { + this.attr3 = attr3; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CrossIndexImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CrossIndexImmutable.java new file mode 100644 index 000000000000..13291c6d14d8 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/CrossIndexImmutable.java @@ -0,0 +1,117 @@ +/* + * 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.mapper.testbeans; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbImmutable(builder = CrossIndexImmutable.Builder.class) +public class CrossIndexImmutable { + private final String id; + private final String attr1; + private final String attr2; + private final String attr3; + + private CrossIndexImmutable(Builder b) { + this.id = b.id; + this.attr1 = b.attr1; + this.attr2 = b.attr2; + this.attr3 = b.attr3; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + @DynamoDbSecondarySortKey(indexNames = "gsi2", order = Order.FIRST) + public String attr1() { + return attr1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String attr2() { + return attr2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi2", order = Order.FIRST) + public String attr3() { + return attr3; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CrossIndexImmutable that = (CrossIndexImmutable) o; + return Objects.equals(id, that.id) && + Objects.equals(attr1, that.attr1) && + Objects.equals(attr2, that.attr2) && + Objects.equals(attr3, that.attr3); + } + + @Override + public int hashCode() { + return Objects.hash(id, attr1, attr2, attr3); + } + + public static final class Builder { + private String id; + private String attr1; + private String attr2; + private String attr3; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder attr1(String attr1) { + this.attr1 = attr1; + return this; + } + + public Builder attr2(String attr2) { + this.attr2 = attr2; + return this; + } + + public Builder attr3(String attr3) { + this.attr3 = attr3; + return this; + } + + public CrossIndexImmutable build() { + return new CrossIndexImmutable(this); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DuplicateOrderBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DuplicateOrderBean.java new file mode 100644 index 000000000000..cfd5c938af84 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DuplicateOrderBean.java @@ -0,0 +1,55 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbBean +public class DuplicateOrderBean { + private String id; + private String key1; + private String key2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DuplicateOrderImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DuplicateOrderImmutable.java new file mode 100644 index 000000000000..01e733c55954 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/DuplicateOrderImmutable.java @@ -0,0 +1,81 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbImmutable(builder = DuplicateOrderImmutable.Builder.class) +public class DuplicateOrderImmutable { + private final String id; + private final String key1; + private final String key2; + + private DuplicateOrderImmutable(Builder b) { + this.id = b.id; + this.key1 = b.key1; + this.key2 = b.key2; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String key1() { + return key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String key2() { + return key2; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String key1; + private String key2; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder key1(String key1) { + this.key1 = key1; + return this; + } + + public Builder key2(String key2) { + this.key2 = key2; + return this; + } + + public DuplicateOrderImmutable build() { + return new DuplicateOrderImmutable(this); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/ImplicitOrderBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/ImplicitOrderBean.java new file mode 100644 index 000000000000..83f727be2788 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/ImplicitOrderBean.java @@ -0,0 +1,64 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbBean +public class ImplicitOrderBean { + private String id; + private String key1; + private String key2; + private String key3; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String getKey3() { + return key3; + } + + public void setKey3(String key3) { + this.key3 = key3; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedCompositeBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedCompositeBean.java new file mode 100644 index 000000000000..0acaec62ebcc --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedCompositeBean.java @@ -0,0 +1,86 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbBean +public class MixedCompositeBean { + private String id; + private String pk1; + private String pk2; + private String sk1; + private String sk2; + private String sk3; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getPk1() { + return pk1; + } + + public void setPk1(String pk1) { + this.pk1 = pk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getPk2() { + return pk2; + } + + public void setPk2(String pk2) { + this.pk2 = pk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.SECOND) + public String getSk1() { + return sk1; + } + + public void setSk1(String sk1) { + this.sk1 = sk1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FIRST) + public String getSk2() { + return sk2; + } + + public void setSk2(String sk2) { + this.sk2 = sk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.THIRD) + public String getSk3() { + return sk3; + } + + public void setSk3(String sk3) { + this.sk3 = sk3; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedFlattenedImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedFlattenedImmutable.java new file mode 100644 index 000000000000..d29cacc3a0e6 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedFlattenedImmutable.java @@ -0,0 +1,182 @@ +/* + * 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.mapper.testbeans; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbFlatten; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbImmutable(builder = MixedFlattenedImmutable.Builder.class) +public class MixedFlattenedImmutable { + private final String id; + private final String rootKey1; + private final String rootKey2; + private final FlattenedKeys flattenedKeys; + + private MixedFlattenedImmutable(Builder b) { + this.id = b.id; + this.rootKey1 = b.rootKey1; + this.rootKey2 = b.rootKey2; + this.flattenedKeys = b.flattenedKeys; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "mixed_gsi", order = Order.FIRST) + public String rootKey1() { + return rootKey1; + } + + @DynamoDbSecondarySortKey(indexNames = "mixed_gsi", order = Order.FIRST) + public String rootKey2() { + return rootKey2; + } + + @DynamoDbFlatten + public FlattenedKeys flattenedKeys() { + return flattenedKeys; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MixedFlattenedImmutable that = (MixedFlattenedImmutable) o; + return Objects.equals(id, that.id) && + Objects.equals(rootKey1, that.rootKey1) && + Objects.equals(rootKey2, that.rootKey2) && + Objects.equals(flattenedKeys, that.flattenedKeys); + } + + @Override + public int hashCode() { + return Objects.hash(id, rootKey1, rootKey2, flattenedKeys); + } + + public static final class Builder { + private String id; + private String rootKey1; + private String rootKey2; + private FlattenedKeys flattenedKeys; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder rootKey1(String rootKey1) { + this.rootKey1 = rootKey1; + return this; + } + + public Builder rootKey2(String rootKey2) { + this.rootKey2 = rootKey2; + return this; + } + + public Builder flattenedKeys(FlattenedKeys flattenedKeys) { + this.flattenedKeys = flattenedKeys; + return this; + } + + public MixedFlattenedImmutable build() { + return new MixedFlattenedImmutable(this); + } + } + + @DynamoDbImmutable(builder = FlattenedKeys.Builder.class) + public static class FlattenedKeys { + private final String flatKey1; + private final String flatKey2; + + private FlattenedKeys(Builder b) { + this.flatKey1 = b.flatKey1; + this.flatKey2 = b.flatKey2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "mixed_gsi", order = Order.SECOND) + public String flatKey1() { + return flatKey1; + } + + @DynamoDbSecondarySortKey(indexNames = "mixed_gsi", order = Order.SECOND) + public String flatKey2() { + return flatKey2; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FlattenedKeys that = (FlattenedKeys) o; + return Objects.equals(flatKey1, that.flatKey1) && + Objects.equals(flatKey2, that.flatKey2); + } + + @Override + public int hashCode() { + return Objects.hash(flatKey1, flatKey2); + } + + public static final class Builder { + private String flatKey1; + private String flatKey2; + + private Builder() { + } + + public Builder flatKey1(String flatKey1) { + this.flatKey1 = flatKey1; + return this; + } + + public Builder flatKey2(String flatKey2) { + this.flatKey2 = flatKey2; + return this; + } + + public FlattenedKeys build() { + return new FlattenedKeys(this); + } + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedOrderingBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedOrderingBean.java new file mode 100644 index 000000000000..6cc2629ef784 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedOrderingBean.java @@ -0,0 +1,55 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbBean +public class MixedOrderingBean { + private String id; + private String key1; + private String key2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedOrderingImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedOrderingImmutable.java new file mode 100644 index 000000000000..87e18f9497b1 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MixedOrderingImmutable.java @@ -0,0 +1,81 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbImmutable(builder = MixedOrderingImmutable.Builder.class) +public class MixedOrderingImmutable { + private final String id; + private final String key1; + private final String key2; + + private MixedOrderingImmutable(Builder b) { + this.id = b.id; + this.key1 = b.key1; + this.key2 = b.key2; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String key1() { + return key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String key2() { + return key2; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String key1; + private String key2; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder key1(String key1) { + this.key1 = key1; + return this; + } + + public Builder key2(String key2) { + this.key2 = key2; + return this; + } + + public MixedOrderingImmutable build() { + return new MixedOrderingImmutable(this); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MultiGSIBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MultiGSIBean.java new file mode 100644 index 000000000000..719d71bf6d43 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/MultiGSIBean.java @@ -0,0 +1,137 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class MultiGSIBean { + private String id; + private String sort; + private String gsi1Pk1; + private String gsi1Pk2; + private String gsi1Sk; + private String gsi2Pk; + private String gsi2Sk1; + private String gsi2Sk2; + private String gsi3Pk1; + private String gsi3Pk2; + private String gsi3Pk3; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getGsi1Pk1() { + return gsi1Pk1; + } + + public void setGsi1Pk1(String gsi1Pk1) { + this.gsi1Pk1 = gsi1Pk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getGsi1Pk2() { + return gsi1Pk2; + } + + public void setGsi1Pk2(String gsi1Pk2) { + this.gsi1Pk2 = gsi1Pk2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1") + public String getGsi1Sk() { + return gsi1Sk; + } + + public void setGsi1Sk(String gsi1Sk) { + this.gsi1Sk = gsi1Sk; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi2") + public String getGsi2Pk() { + return gsi2Pk; + } + + public void setGsi2Pk(String gsi2Pk) { + this.gsi2Pk = gsi2Pk; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi2", order = Order.FIRST) + public String getGsi2Sk1() { + return gsi2Sk1; + } + + public void setGsi2Sk1(String gsi2Sk1) { + this.gsi2Sk1 = gsi2Sk1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi2", order = Order.SECOND) + public String getGsi2Sk2() { + return gsi2Sk2; + } + + public void setGsi2Sk2(String gsi2Sk2) { + this.gsi2Sk2 = gsi2Sk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi3", order = Order.FIRST) + public String getGsi3Pk1() { + return gsi3Pk1; + } + + public void setGsi3Pk1(String gsi3Pk1) { + this.gsi3Pk1 = gsi3Pk1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi3", order = Order.SECOND) + public String getGsi3Pk2() { + return gsi3Pk2; + } + + public void setGsi3Pk2(String gsi3Pk2) { + this.gsi3Pk2 = gsi3Pk2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi3", order = Order.THIRD) + public String getGsi3Pk3() { + return gsi3Pk3; + } + + public void setGsi3Pk3(String gsi3Pk3) { + this.gsi3Pk3 = gsi3Pk3; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NonSequentialOrderBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NonSequentialOrderBean.java new file mode 100644 index 000000000000..0d84b5e27659 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NonSequentialOrderBean.java @@ -0,0 +1,55 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbBean +public class NonSequentialOrderBean { + private String id; + private String key1; + private String key2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.THIRD) // Gap in sequence + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NonSequentialOrderImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NonSequentialOrderImmutable.java new file mode 100644 index 000000000000..355313e983b9 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/NonSequentialOrderImmutable.java @@ -0,0 +1,81 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbImmutable(builder = NonSequentialOrderImmutable.Builder.class) +public class NonSequentialOrderImmutable { + private final String id; + private final String key1; + private final String key2; + + private NonSequentialOrderImmutable(Builder b) { + this.id = b.id; + this.key1 = b.key1; + this.key2 = b.key2; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String key1() { + return key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.THIRD) + public String key2() { + return key2; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String id; + private String key1; + private String key2; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder key1(String key1) { + this.key1 = key1; + return this; + } + + public Builder key2(String key2) { + this.key2 = key2; + return this; + } + + public NonSequentialOrderImmutable build() { + return new NonSequentialOrderImmutable(this); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/OrderPreservationBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/OrderPreservationBean.java new file mode 100644 index 000000000000..78ab43c16103 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/OrderPreservationBean.java @@ -0,0 +1,75 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbBean +public class OrderPreservationBean { + private String id; + private String key1; + private String key2; + private String key3; + private String key4; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FOURTH) + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getKey3() { + return key3; + } + + public void setKey3(String key3) { + this.key3 = key3; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.THIRD) + public String getKey4() { + return key4; + } + + public void setKey4(String key4) { + this.key4 = key4; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/OrderPreservationImmutable.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/OrderPreservationImmutable.java new file mode 100644 index 000000000000..a7e22ea3840a --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/OrderPreservationImmutable.java @@ -0,0 +1,129 @@ +/* + * 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.mapper.testbeans; + +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbImmutable; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbImmutable(builder = OrderPreservationImmutable.Builder.class) +public class OrderPreservationImmutable { + private final String id; + private final String key1; + private final String key2; + private final String key3; + private final String key4; + + private OrderPreservationImmutable(Builder b) { + this.id = b.id; + this.key1 = b.key1; + this.key2 = b.key2; + this.key3 = b.key3; + this.key4 = b.key4; + } + + @DynamoDbPartitionKey + public String id() { + return id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FOURTH) + public String key1() { + return key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String key2() { + return key2; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String key3() { + return key3; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.THIRD) + public String key4() { + return key4; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + OrderPreservationImmutable that = (OrderPreservationImmutable) o; + return Objects.equals(id, that.id) && + Objects.equals(key1, that.key1) && + Objects.equals(key2, that.key2) && + Objects.equals(key3, that.key3) && + Objects.equals(key4, that.key4); + } + + @Override + public int hashCode() { + return Objects.hash(id, key1, key2, key3, key4); + } + + public static final class Builder { + private String id; + private String key1; + private String key2; + private String key3; + private String key4; + + private Builder() { + } + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder key1(String key1) { + this.key1 = key1; + return this; + } + + public Builder key2(String key2) { + this.key2 = key2; + return this; + } + + public Builder key3(String key3) { + this.key3 = key3; + return this; + } + + public Builder key4(String key4) { + this.key4 = key4; + return this; + } + + public OrderPreservationImmutable build() { + return new OrderPreservationImmutable(this); + } + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SingleKeyBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SingleKeyBean.java new file mode 100644 index 000000000000..e9c402737866 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/SingleKeyBean.java @@ -0,0 +1,66 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; + +@DynamoDbBean +public class SingleKeyBean { + private String id; + private String sort; + private String gsiPk; + private String gsiSk; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSortKey + public String getSort() { + return sort; + } + + public void setSort(String sort) { + this.sort = sort; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1") + public String getGsiPk() { + return gsiPk; + } + + public void setGsiPk(String gsiPk) { + this.gsiPk = gsiPk; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1") + public String getGsiSk() { + return gsiSk; + } + + public void setGsiSk(String gsiSk) { + this.gsiSk = gsiSk; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/ThreeSortKeyBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/ThreeSortKeyBean.java new file mode 100644 index 000000000000..3b7d23cabe85 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/ThreeSortKeyBean.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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondarySortKey; + +@DynamoDbBean +public class ThreeSortKeyBean { + private String id; + private String sort1; + private String sort2; + private String sort3; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.THIRD) + public String getSort1() { + return sort1; + } + + public void setSort1(String sort1) { + this.sort1 = sort1; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.FIRST) + public String getSort2() { + return sort2; + } + + public void setSort2(String sort2) { + this.sort2 = sort2; + } + + @DynamoDbSecondarySortKey(indexNames = "gsi1", order = Order.SECOND) + public String getSort3() { + return sort3; + } + + public void setSort3(String sort3) { + this.sort3 = sort3; + } +} \ No newline at end of file diff --git a/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TwoPartitionKeyBean.java b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TwoPartitionKeyBean.java new file mode 100644 index 000000000000..41db6cbabe98 --- /dev/null +++ b/services-custom/dynamodb-enhanced/src/test/java/software/amazon/awssdk/enhanced/dynamodb/mapper/testbeans/TwoPartitionKeyBean.java @@ -0,0 +1,55 @@ +/* + * 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.mapper.testbeans; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.Order; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; + +@DynamoDbBean +public class TwoPartitionKeyBean { + private String id; + private String key1; + private String key2; + + @DynamoDbPartitionKey + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.SECOND) + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + @DynamoDbSecondaryPartitionKey(indexNames = "gsi1", order = Order.FIRST) + public String getKey2() { + return key2; + } + + public void setKey2(String key2) { + this.key2 = key2; + } +} \ No newline at end of file