/* * Copyright 2010-2015 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. * You may obtain a copy of the License at: * * http://aws.amazon.com/apache2.0 * * 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 com.amazonaws.mobileconnectors.dynamodbv2.dynamodbmapper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.ComparisonOperator; import com.amazonaws.services.dynamodbv2.model.Condition; import com.amazonaws.services.dynamodbv2.model.QueryRequest; import com.amazonaws.util.ImmutableMapParameter; import org.junit.BeforeClass; import org.junit.Test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * Unit test for the private method * DynamoDBMapper#createQueryRequestFromExpression */ public class MapperQueryExpressionTest { private static final String TABLE_NAME = "table_name"; private static final Condition RANGE_KEY_CONDITION = new Condition() .withAttributeValueList(new AttributeValue("some value")) .withComparisonOperator(ComparisonOperator.EQ); private static DynamoDBMapper mapper; private static Method testedMethod; @BeforeClass public static void setUp() throws SecurityException, NoSuchMethodException { AmazonDynamoDB dynamo = new AmazonDynamoDBClient(); mapper = new DynamoDBMapper(dynamo); testedMethod = DynamoDBMapper.class.getDeclaredMethod("createQueryRequestFromExpression", Class.class, DynamoDBQueryExpression.class, DynamoDBMapperConfig.class); testedMethod.setAccessible(true); } @DynamoDBTable(tableName = TABLE_NAME) public final class HashOnlyClass { @DynamoDBHashKey @DynamoDBIndexHashKey( globalSecondaryIndexNames = "GSI-primary-hash") private String primaryHashKey; @DynamoDBIndexHashKey( globalSecondaryIndexNames = { "GSI-index-hash-1", "GSI-index-hash-2" }) private String indexHashKey; @DynamoDBIndexHashKey( globalSecondaryIndexNames = { "GSI-another-index-hash" }) private String anotherIndexHashKey; public HashOnlyClass(String primaryHashKey, String indexHashKey, String anotherIndexHashKey) { this.primaryHashKey = primaryHashKey; this.indexHashKey = indexHashKey; this.anotherIndexHashKey = anotherIndexHashKey; } public String getPrimaryHashKey() { return primaryHashKey; } public void setPrimaryHashKey(String primaryHashKey) { this.primaryHashKey = primaryHashKey; } public String getIndexHashKey() { return indexHashKey; } public void setIndexHashKey(String indexHashKey) { this.indexHashKey = indexHashKey; } public String getAnotherIndexHashKey() { return anotherIndexHashKey; } public void setAnotherIndexHashKey(String anotherIndexHashKey) { this.anotherIndexHashKey = anotherIndexHashKey; } } /** Tests different scenarios of hash-only query **/ @Test public void testHashConditionOnly() { // Primary hash only QueryRequest queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass("foo", null, null))); assertTrue(queryRequest.getKeyConditions().size() == 1); assertEquals("primaryHashKey", queryRequest.getKeyConditions().keySet().iterator().next()); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertNull(queryRequest.getIndexName()); // Primary hash used for a GSI queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass("foo", null, null)) .withIndexName("GSI-primary-hash")); assertTrue(queryRequest.getKeyConditions().size() == 1); assertEquals("primaryHashKey", queryRequest.getKeyConditions().keySet().iterator().next()); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertEquals("GSI-primary-hash", queryRequest.getIndexName()); // Primary hash query takes higher priority then index hash query queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass("foo", "bar", null))); assertTrue(queryRequest.getKeyConditions().size() == 1); assertEquals("primaryHashKey", queryRequest.getKeyConditions().keySet().iterator().next()); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertNull(queryRequest.getIndexName()); // Ambiguous query on multiple index hash keys queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass(null, "bar", "charlie")), "Ambiguous query expression: More than one index hash key EQ conditions"); // Ambiguous query when not specifying index name queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass(null, "bar", null)), "Ambiguous query expression: More than one GSIs"); // Explicitly specify a GSI. queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass("foo", "bar", null)) .withIndexName("GSI-index-hash-1")); assertTrue(queryRequest.getKeyConditions().size() == 1); assertEquals("indexHashKey", queryRequest.getKeyConditions().keySet().iterator().next()); assertEquals( new Condition().withAttributeValueList(new AttributeValue("bar")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("indexHashKey")); assertEquals("GSI-index-hash-1", queryRequest.getIndexName()); // Non-existent GSI queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass("foo", "bar", null)) .withIndexName("some fake gsi"), "No hash key condition is applicable to the specified index"); // No hash key condition specified queryRequest = testCreateQueryRequestFromExpression( HashOnlyClass.class, new DynamoDBQueryExpression<HashOnlyClass>() .withHashKeyValues(new HashOnlyClass(null, null, null)), "Illegal query expression: No hash key condition is found in the query"); } @DynamoDBTable(tableName = TABLE_NAME) public final class HashRangeClass { private String primaryHashKey; private String indexHashKey; private String primaryRangeKey; private String indexRangeKey; private String anotherIndexRangeKey; public HashRangeClass(String primaryHashKey, String indexHashKey) { this.primaryHashKey = primaryHashKey; this.indexHashKey = indexHashKey; } @DynamoDBHashKey @DynamoDBIndexHashKey( globalSecondaryIndexNames = { "GSI-primary-hash-index-range-1", "GSI-primary-hash-index-range-2" } ) public String getPrimaryHashKey() { return primaryHashKey; } public void setPrimaryHashKey(String primaryHashKey) { this.primaryHashKey = primaryHashKey; } @DynamoDBIndexHashKey( globalSecondaryIndexNames = { "GSI-index-hash-primary-range", "GSI-index-hash-index-range-1", "GSI-index-hash-index-range-2" } ) public String getIndexHashKey() { return indexHashKey; } public void setIndexHashKey(String indexHashKey) { this.indexHashKey = indexHashKey; } @DynamoDBRangeKey @DynamoDBIndexRangeKey( globalSecondaryIndexNames = { "GSI-index-hash-primary-range" }, localSecondaryIndexName = "LSI-primary-range" ) public String getPrimaryRangeKey() { return primaryRangeKey; } public void setPrimaryRangeKey(String primaryRangeKey) { this.primaryRangeKey = primaryRangeKey; } @DynamoDBIndexRangeKey( globalSecondaryIndexNames = { "GSI-primary-hash-index-range-1", "GSI-index-hash-index-range-1", "GSI-index-hash-index-range-2" }, localSecondaryIndexNames = { "LSI-index-range-1", "LSI-index-range-2" } ) public String getIndexRangeKey() { return indexRangeKey; } public void setIndexRangeKey(String indexRangeKey) { this.indexRangeKey = indexRangeKey; } @DynamoDBIndexRangeKey( localSecondaryIndexName = "LSI-index-range-3", globalSecondaryIndexName = "GSI-primary-hash-index-range-2" ) public String getAnotherIndexRangeKey() { return anotherIndexRangeKey; } public void setAnotherIndexRangeKey(String anotherIndexRangeKey) { this.anotherIndexRangeKey = anotherIndexRangeKey; } } /** Tests hash + range query **/ @Test public void testHashAndRangeCondition() { // Primary hash + primary range QueryRequest queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("primaryRangeKey", RANGE_KEY_CONDITION)); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("primaryRangeKey")); assertEquals(RANGE_KEY_CONDITION, queryRequest.getKeyConditions().get("primaryRangeKey")); assertNull(queryRequest.getIndexName()); // Primary hash + primary range on a LSI queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("primaryRangeKey", RANGE_KEY_CONDITION) .withIndexName("LSI-primary-range")); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("primaryRangeKey")); assertEquals(RANGE_KEY_CONDITION, queryRequest.getKeyConditions().get("primaryRangeKey")); assertEquals("LSI-primary-range", queryRequest.getIndexName()); // Primary hash + index range used by multiple LSI. But also a GSI hash // + range queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION)); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("indexRangeKey")); assertEquals(RANGE_KEY_CONDITION, queryRequest.getKeyConditions().get("indexRangeKey")); assertEquals("GSI-primary-hash-index-range-1", queryRequest.getIndexName()); // Primary hash + index range on a LSI queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) .withIndexName("LSI-index-range-1")); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("primaryHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("indexRangeKey")); assertEquals(RANGE_KEY_CONDITION, queryRequest.getKeyConditions().get("indexRangeKey")); assertEquals("LSI-index-range-1", queryRequest.getIndexName()); // Non-existent LSI queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) .withIndexName("some fake lsi"), "No range key condition is applicable to the specified index"); // Illegal query: Primary hash + primary range on a GSI queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) .withIndexName("GSI-index-hash-index-range-1"), "Illegal query expression: No hash key condition is applicable to the specified index"); // GSI hash + GSI range queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass(null, "foo")) .withRangeKeyCondition("primaryRangeKey", RANGE_KEY_CONDITION)); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("indexHashKey")); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("indexHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("primaryRangeKey")); assertEquals(RANGE_KEY_CONDITION, queryRequest.getKeyConditions().get("primaryRangeKey")); assertEquals("GSI-index-hash-primary-range", queryRequest.getIndexName()); // Ambiguous query: GSI hash + index range used by multiple GSIs queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass(null, "foo")) .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION), "Illegal query expression: Cannot infer the index name from the query expression."); // Explicitly specify the GSI name queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass(null, "foo")) .withRangeKeyCondition("indexRangeKey", RANGE_KEY_CONDITION) .withIndexName("GSI-index-hash-index-range-2")); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("indexHashKey")); assertEquals( new Condition().withAttributeValueList(new AttributeValue("foo")) .withComparisonOperator(ComparisonOperator.EQ), queryRequest.getKeyConditions().get("indexHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("indexRangeKey")); assertEquals(RANGE_KEY_CONDITION, queryRequest.getKeyConditions().get("indexRangeKey")); assertEquals("GSI-index-hash-index-range-2", queryRequest.getIndexName()); // Ambiguous query: (1) primary hash + LSI range OR (2) GSI hash + range queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("anotherIndexRangeKey", RANGE_KEY_CONDITION), "Ambiguous query expression: Found multiple valid queries:"); // Multiple range key conditions specified queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyConditions( ImmutableMapParameter.of( "primaryRangeKey", RANGE_KEY_CONDITION, "indexRangeKey", RANGE_KEY_CONDITION)), "Illegal query expression: Conditions on multiple range keys"); // Using an un-annotated range key queryRequest = testCreateQueryRequestFromExpression( HashRangeClass.class, new DynamoDBQueryExpression<HashRangeClass>() .withHashKeyValues(new HashRangeClass("foo", null)) .withRangeKeyCondition("indexHashKey", RANGE_KEY_CONDITION), "not annotated with either @DynamoDBRangeKey or @DynamoDBIndexRangeKey."); } @DynamoDBTable(tableName = TABLE_NAME) public final class LSIRangeKeyClass { private String primaryHashKey; private String primaryRangeKey; private String lsiRangeKey; public LSIRangeKeyClass(String primaryHashKey, String primaryRangeKey) { this.primaryHashKey = primaryHashKey; this.primaryRangeKey = primaryRangeKey; } @DynamoDBHashKey public String getPrimaryHashKey() { return primaryHashKey; } public void setPrimaryHashKey(String primaryHashKey) { this.primaryHashKey = primaryHashKey; } @DynamoDBRangeKey public String getPrimaryRangeKey() { return primaryRangeKey; } public void setPrimaryRangeKey(String primaryRangeKey) { this.primaryRangeKey = primaryRangeKey; } @DynamoDBIndexRangeKey(localSecondaryIndexName = "LSI") public String getLsiRangeKey() { return lsiRangeKey; } public void setLsiRangeKey(String lsiRangeKey) { this.lsiRangeKey = lsiRangeKey; } } @Test public void testHashOnlyQueryOnHashRangeTable() { // Primary hash only query on a Hash+Range table QueryRequest queryRequest = testCreateQueryRequestFromExpression( LSIRangeKeyClass.class, new DynamoDBQueryExpression<LSIRangeKeyClass>() .withHashKeyValues(new LSIRangeKeyClass("foo", null))); assertTrue(queryRequest.getKeyConditions().size() == 1); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertNull(queryRequest.getIndexName()); // Hash+Range query on a LSI queryRequest = testCreateQueryRequestFromExpression( LSIRangeKeyClass.class, new DynamoDBQueryExpression<LSIRangeKeyClass>() .withHashKeyValues(new LSIRangeKeyClass("foo", null)) .withRangeKeyCondition("lsiRangeKey", RANGE_KEY_CONDITION) .withIndexName("LSI")); assertTrue(queryRequest.getKeyConditions().size() == 2); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertTrue(queryRequest.getKeyConditions().containsKey("lsiRangeKey")); assertEquals("LSI", queryRequest.getIndexName()); // Hash-only query on a LSI queryRequest = testCreateQueryRequestFromExpression( LSIRangeKeyClass.class, new DynamoDBQueryExpression<LSIRangeKeyClass>() .withHashKeyValues(new LSIRangeKeyClass("foo", null)) .withIndexName("LSI")); assertTrue(queryRequest.getKeyConditions().size() == 1); assertTrue(queryRequest.getKeyConditions().containsKey("primaryHashKey")); assertEquals("LSI", queryRequest.getIndexName()); } private static <T> QueryRequest testCreateQueryRequestFromExpression( Class<T> clazz, DynamoDBQueryExpression<T> queryExpression) { return testCreateQueryRequestFromExpression(clazz, queryExpression, null); } private static <T> QueryRequest testCreateQueryRequestFromExpression( Class<T> clazz, DynamoDBQueryExpression<T> queryExpression, String expectedErrorMessage) { try { QueryRequest request = (QueryRequest) testedMethod.invoke(mapper, clazz, queryExpression, DynamoDBMapperConfig.DEFAULT); if (expectedErrorMessage != null) { fail("Exception containing messsage (" + expectedErrorMessage + ") is expected."); } return request; } catch (InvocationTargetException ite) { if (expectedErrorMessage != null) { assertTrue("Exception message [" + ite.getCause().getMessage() + "] does not contain " + "the expected message [" + expectedErrorMessage + "].", ite.getCause().getMessage().contains(expectedErrorMessage)); } else { ite.getCause().printStackTrace(); fail("Internal error when calling createQueryRequestFromExpressio method"); } } catch (Exception e) { fail(e.getMessage()); } return null; } }