diff --git a/moto/dynamodb/comparisons.py b/moto/dynamodb/comparisons.py index 3bbbeae3c..1a0db2379 100644 --- a/moto/dynamodb/comparisons.py +++ b/moto/dynamodb/comparisons.py @@ -6,13 +6,13 @@ COMPARISON_FUNCS = { 'LT': lambda item_value, test_value: item_value < test_value, 'GE': lambda item_value, test_value: item_value >= test_value, 'GT': lambda item_value, test_value: item_value > test_value, - 'NULL': lambda item_value: item_value is None, - 'NOT_NULL': lambda item_value: item_value is not None, + 'NULL': lambda item_value, test_value: item_value is None, + 'NOT_NULL': lambda item_value, test_value: item_value is not None, 'CONTAINS': lambda item_value, test_value: test_value in item_value, 'NOT_CONTAINS': lambda item_value, test_value: test_value not in item_value, 'BEGINS_WITH': lambda item_value, test_value: item_value.startswith(test_value), 'IN': lambda item_value, test_value: item_value in test_value, - 'BETWEEN': lambda item_value, test_values: test_values[0] <= item_value <= test_values[1], + 'BETWEEN': lambda item_value, lower_test_value, upper_test_value: lower_test_value <= item_value <= upper_test_value, } diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index 5dac91f21..d211f9e20 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -97,14 +97,14 @@ class Table(object): except KeyError: return None - def query(self, hash_key, range_comparison, range_value): + def query(self, hash_key, range_comparison, range_values): results = [] last_page = True # Once pagination is implemented, change this possible_results = self.items.get(hash_key, []) comparison_func = get_comparison_func(range_comparison) for result in possible_results.values(): - if comparison_func(result.range_key, range_value): + if comparison_func(result.range_key, *range_values): results.append(result) return results, last_page @@ -121,12 +121,24 @@ class Table(object): for result in self.all_items(): scanned_count += 1 passes_all_conditions = True - for attribute_name, (comparison_operator, comparison_value) in filters.iteritems(): + for attribute_name, (comparison_operator, comparison_values) in filters.iteritems(): comparison_func = get_comparison_func(comparison_operator) - attribute_value = result.attrs[attribute_name].values()[0] - if not comparison_func(attribute_value, comparison_value): + + attribute = result.attrs.get(attribute_name) + if attribute: + # Attribute found + attribute_value = attribute.values()[0] + if not comparison_func(attribute_value, *comparison_values): + passes_all_conditions = False + break + elif comparison_operator == 'NULL': + # Comparison is NULL and we don't have the attribute + continue + else: + # No attribute found and comparison is no NULL. This item fails passes_all_conditions = False break + if passes_all_conditions: results.append(result) @@ -172,12 +184,12 @@ class DynamoDBBackend(BaseBackend): return table.get_item(hash_key, range_key) - def query(self, table_name, hash_key, range_comparison, range_value): + def query(self, table_name, hash_key, range_comparison, range_values): table = self.tables.get(table_name) if not table: return None - return table.query(hash_key, range_comparison, range_value) + return table.query(hash_key, range_comparison, range_values) def scan(self, table_name, filters): table = self.tables.get(table_name) diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index a6dd95812..378743607 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -2,6 +2,7 @@ import json from moto.core.utils import headers_to_dict from .models import dynamodb_backend +from .utils import values_from_dynamo_types class DynamoHandler(object): @@ -128,8 +129,9 @@ class DynamoHandler(object): hash_key = body['HashKeyValue'].values()[0] range_condition = body['RangeKeyCondition'] range_comparison = range_condition['ComparisonOperator'] - range_value = range_condition['AttributeValueList'][0].values()[0] - items, last_page = dynamodb_backend.query(name, hash_key, range_comparison, range_value) + range_values = values_from_dynamo_types(range_condition['AttributeValueList']) + + items, last_page = dynamodb_backend.query(name, hash_key, range_comparison, range_values) result = { "Count": len(items), @@ -152,8 +154,11 @@ class DynamoHandler(object): for attribute_name, scan_filter in scan_filters.iteritems(): # Keys are attribute names. Values are tuples of (comparison, comparison_value) comparison_operator = scan_filter["ComparisonOperator"] - comparison_value = scan_filter["AttributeValueList"][0].values()[0] - filters[attribute_name] = (comparison_operator, comparison_value) + if scan_filter.get("AttributeValueList"): + comparison_values = values_from_dynamo_types(scan_filter.get("AttributeValueList")) + else: + comparison_values = [None] + filters[attribute_name] = (comparison_operator, comparison_values) items, scanned_count, last_page = dynamodb_backend.scan(name, filters) diff --git a/moto/dynamodb/utils.py b/moto/dynamodb/utils.py index e4787d105..872ddbc23 100644 --- a/moto/dynamodb/utils.py +++ b/moto/dynamodb/utils.py @@ -5,3 +5,18 @@ def unix_time(dt): epoch = datetime.datetime.utcfromtimestamp(0) delta = dt - epoch return delta.total_seconds() + + +def value_from_dynamo_type(dynamo_type): + """ + Dynamo return attributes like {"S": "AttributeValue1"}. + This function takes that value and returns "AttributeValue1". + + # TODO eventually this should be smarted to actually read the type of + the attribute + """ + return dynamo_type.values()[0] + + +def values_from_dynamo_types(dynamo_types): + return [value_from_dynamo_type(dynamo_type) for dynamo_type in dynamo_types] diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py index ef34359c8..2f50b04b9 100644 --- a/tests/test_dynamodb/test_dynamodb.py +++ b/tests/test_dynamodb/test_dynamodb.py @@ -224,6 +224,15 @@ def test_query(): results = table.query(hash_key='the-key', range_key_condition=condition.GT('9999')) results.response['Items'].should.have.length_of(0) + results = table.query(hash_key='the-key', range_key_condition=condition.CONTAINS('12')) + results.response['Items'].should.have.length_of(1) + + results = table.query(hash_key='the-key', range_key_condition=condition.BEGINS_WITH('7')) + results.response['Items'].should.have.length_of(1) + + results = table.query(hash_key='the-key', range_key_condition=condition.BETWEEN('567', '890')) + results.response['Items'].should.have.length_of(1) + @mock_dynamodb def test_scan(): @@ -249,6 +258,13 @@ def test_scan(): ) item.put() + item_data = { + 'Body': 'http://url_to_lolcat.gif', + 'SentBy': 'User B', + 'ReceivedTime': '12/9/2011 11:36:03 PM', + 'Ids': {1, 2, 3}, + 'PK': 7, + } item = table.new_item( hash_key='the-key', range_key='789', @@ -257,12 +273,26 @@ def test_scan(): item.put() results = table.scan(scan_filter={'SentBy': condition.EQ('User B')}) - results.response['Items'].should.have.length_of(0) + results.response['Items'].should.have.length_of(1) results = table.scan(scan_filter={'Body': condition.BEGINS_WITH('http')}) results.response['Items'].should.have.length_of(3) + results = table.scan(scan_filter={'Ids': condition.CONTAINS(2)}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Ids': condition.NOT_NULL()}) + results.response['Items'].should.have.length_of(1) + + results = table.scan(scan_filter={'Ids': condition.NULL()}) + results.response['Items'].should.have.length_of(2) + + results = table.scan(scan_filter={'PK': condition.BETWEEN(8, 9)}) + results.response['Items'].should.have.length_of(0) + + results = table.scan(scan_filter={'PK': condition.BETWEEN(5, 8)}) + results.response['Items'].should.have.length_of(1) + # Batch read # Batch write -# scan