From 82f19952dda7a0b627eba0dcfd733c15bba3f4aa Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sat, 14 Mar 2015 15:02:43 -0400 Subject: [PATCH] Add Dynamodb2 global indexes. Closes #329. --- moto/dynamodb2/models.py | 35 ++++++- moto/dynamodb2/responses.py | 15 +-- .../test_dynamodb_table_without_range_key.py | 2 +- .../test_dynamodb_table_with_range_key.py | 98 ++++++++++++++++++- .../test_dynamodb_table_without_range_key.py | 3 +- 5 files changed, 137 insertions(+), 16 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index bc79b7e9..e679e0fd 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -122,7 +122,7 @@ class Item(object): class Table(object): - def __init__(self, table_name, schema=None, attr=None, throughput=None, indexes=None): + def __init__(self, table_name, schema=None, attr=None, throughput=None, indexes=None, global_indexes=None): self.name = table_name self.attr = attr self.schema = schema @@ -143,6 +143,7 @@ class Table(object): self.throughput = throughput self.throughput["NumberOfDecreasesToday"] = 0 self.indexes = indexes + self.global_indexes = global_indexes if global_indexes else [] self.created_at = datetime.datetime.now() self.items = defaultdict(dict) @@ -158,6 +159,7 @@ class Table(object): 'KeySchema': self.schema, 'ItemCount': len(self), 'CreationDateTime': unix_time(self.created_at), + 'GlobalSecondaryIndexes': [index for index in self.global_indexes], } } return results @@ -171,6 +173,24 @@ class Table(object): count += 1 return count + @property + def hash_key_names(self): + keys = [self.hash_key_attr] + for index in self.global_indexes: + for key in index['KeySchema']: + if key['KeyType'] == 'HASH': + keys.append(key['AttributeName']) + return keys + + @property + def range_key_names(self): + keys = [self.range_key_attr] + for index in self.global_indexes: + for key in index['KeySchema']: + if key['KeyType'] == 'RANGE': + keys.append(key['AttributeName']) + return keys + def put_item(self, item_attrs): hash_value = DynamoType(item_attrs.get(self.hash_key_attr)) if self.has_range_key: @@ -293,12 +313,21 @@ class DynamoDBBackend(BaseBackend): return None return table.put_item(item_attrs) - def get_table_keys_name(self, table_name): + def get_table_keys_name(self, table_name, keys): + """ + Given a set of keys, extracts the key and range key + """ table = self.tables.get(table_name) if not table: return None, None else: - return table.hash_key_attr, table.range_key_attr + hash_key = range_key = None + for key in keys: + if key in table.hash_key_names: + hash_key = key + elif key in table.range_key_names: + range_key = key + return hash_key, range_key def get_keys_value(self, table, keys): if table.hash_key_attr not in keys or (table.has_range_key and table.range_key_attr not in keys): diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 4cc064bf..8cee08eb 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -99,10 +99,12 @@ class DynamoHandler(BaseResponse): # getting attribute definition attr = body["AttributeDefinitions"] # getting the indexes + global_indexes = body.get("GlobalSecondaryIndexes", []) table = dynamodb_backend2.create_table(table_name, schema=key_schema, throughput=throughput, - attr=attr) + attr=attr, + global_indexes=global_indexes) return dynamo_json_dump(table.describe) def delete_table(self): @@ -216,13 +218,14 @@ class DynamoHandler(BaseResponse): def query(self): name = self.body['TableName'] - keys = self.body['KeyConditions'] - hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name) + key_conditions = self.body['KeyConditions'] + hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name, key_conditions.keys()) + # hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name) if hash_key_name is None: er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" return self.error(er) - hash_key = keys[hash_key_name]['AttributeValueList'][0] - if len(keys) == 1: + hash_key = key_conditions[hash_key_name]['AttributeValueList'][0] + if len(key_conditions) == 1: range_comparison = None range_values = [] else: @@ -230,7 +233,7 @@ class DynamoHandler(BaseResponse): er = "com.amazon.coral.validate#ValidationException" return self.error(er) else: - range_condition = keys[range_key_name] + range_condition = key_conditions[range_key_name] if range_condition: range_comparison = range_condition['ComparisonOperator'] range_values = range_condition['AttributeValueList'] diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py index a23c2d47..eafccb3d 100644 --- a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py @@ -48,7 +48,7 @@ def test_create_table(): }, 'TableName': 'messages', 'TableSizeBytes': 0, - 'TableStatus': 'ACTIVE' + 'TableStatus': 'ACTIVE', } } conn.describe_table('messages').should.equal(expected) diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index e786d2e2..496aaed1 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -7,10 +7,8 @@ from moto import mock_dynamodb2 from boto.exception import JSONResponseError from tests.helpers import requires_boto_gte try: - from boto.dynamodb2.fields import HashKey - from boto.dynamodb2.fields import RangeKey - from boto.dynamodb2.table import Table - from boto.dynamodb2.table import Item + from boto.dynamodb2.fields import GlobalAllIndex, HashKey, RangeKey + from boto.dynamodb2.table import Item, Table from boto.dynamodb2.exceptions import ValidationException except ImportError: pass @@ -53,7 +51,8 @@ def test_create_table(): {'KeyType': 'HASH', 'AttributeName': 'forum_name'}, {'KeyType': 'RANGE', 'AttributeName': 'subject'} ], - 'ItemCount': 0, 'CreationDateTime': 1326499200.0 + 'ItemCount': 0, 'CreationDateTime': 1326499200.0, + 'GlobalSecondaryIndexes': [], } } table.describe().should.equal(expected) @@ -445,3 +444,92 @@ def test_get_key_fields(): table = create_table() kf = table.get_key_fields() kf.should.equal(['forum_name', 'subject']) + + +@mock_dynamodb2 +def test_create_with_global_indexes(): + conn = boto.dynamodb2.layer1.DynamoDBConnection() + + Table.create('messages', schema=[ + HashKey('subject'), + RangeKey('version'), + ], global_indexes=[ + GlobalAllIndex('topic-created_at-index', + parts=[ + HashKey('topic'), + RangeKey('created_at', data_type='N') + ], + throughput={ + 'read': 6, + 'write': 1 + } + ), + ]) + + table_description = conn.describe_table("messages") + table_description['Table']["GlobalSecondaryIndexes"].should.equal([ + { + "IndexName": "topic-created_at-index", + "KeySchema": [ + { + "AttributeName": "topic", + "KeyType": "HASH" + }, + { + "AttributeName": "created_at", + "KeyType": "RANGE" + }, + ], + "Projection": { + "ProjectionType": "ALL" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 6, + "WriteCapacityUnits": 1, + } + } + ]) + + +@mock_dynamodb2 +def test_query_with_global_indexes(): + table = Table.create('messages', schema=[ + HashKey('subject'), + RangeKey('version'), + ], global_indexes=[ + GlobalAllIndex('topic-created_at-index', + parts=[ + HashKey('topic'), + RangeKey('created_at', data_type='N') + ], + throughput={ + 'read': 6, + 'write': 1 + } + ), + GlobalAllIndex('status-created_at-index', + parts=[ + HashKey('status'), + RangeKey('created_at', data_type='N') + ], + throughput={ + 'read': 2, + 'write': 1 + } + ) + ]) + + item_data = { + 'subject': 'Check this out!', + 'version': '1', + 'created_at': 0, + 'status': 'inactive' + } + item = Item(table, item_data) + item.save(overwrite=True) + + item['version'] = '2' + item.save(overwrite=True) + + results = table.query(status__eq='active') + list(results).should.have.length_of(0) diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 9c68b0cd..87e56f8d 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -43,7 +43,8 @@ def test_create_table(): 'KeySchema': [ {'KeyType': 'HASH', 'AttributeName': 'forum_name'} ], - 'ItemCount': 0, 'CreationDateTime': 1326499200.0 + 'ItemCount': 0, 'CreationDateTime': 1326499200.0, + 'GlobalSecondaryIndexes': [], } } conn = boto.dynamodb2.connect_to_region(