Add Dynamodb2 global indexes. Closes #329.

This commit is contained in:
Steve Pulec 2015-03-14 15:02:43 -04:00
parent 09fe37da7e
commit 82f19952dd
5 changed files with 137 additions and 16 deletions

View File

@ -122,7 +122,7 @@ class Item(object):
class Table(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.name = table_name
self.attr = attr self.attr = attr
self.schema = schema self.schema = schema
@ -143,6 +143,7 @@ class Table(object):
self.throughput = throughput self.throughput = throughput
self.throughput["NumberOfDecreasesToday"] = 0 self.throughput["NumberOfDecreasesToday"] = 0
self.indexes = indexes self.indexes = indexes
self.global_indexes = global_indexes if global_indexes else []
self.created_at = datetime.datetime.now() self.created_at = datetime.datetime.now()
self.items = defaultdict(dict) self.items = defaultdict(dict)
@ -158,6 +159,7 @@ class Table(object):
'KeySchema': self.schema, 'KeySchema': self.schema,
'ItemCount': len(self), 'ItemCount': len(self),
'CreationDateTime': unix_time(self.created_at), 'CreationDateTime': unix_time(self.created_at),
'GlobalSecondaryIndexes': [index for index in self.global_indexes],
} }
} }
return results return results
@ -171,6 +173,24 @@ class Table(object):
count += 1 count += 1
return count 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): def put_item(self, item_attrs):
hash_value = DynamoType(item_attrs.get(self.hash_key_attr)) hash_value = DynamoType(item_attrs.get(self.hash_key_attr))
if self.has_range_key: if self.has_range_key:
@ -293,12 +313,21 @@ class DynamoDBBackend(BaseBackend):
return None return None
return table.put_item(item_attrs) 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) table = self.tables.get(table_name)
if not table: if not table:
return None, None return None, None
else: 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): 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): if table.hash_key_attr not in keys or (table.has_range_key and table.range_key_attr not in keys):

View File

@ -99,10 +99,12 @@ class DynamoHandler(BaseResponse):
# getting attribute definition # getting attribute definition
attr = body["AttributeDefinitions"] attr = body["AttributeDefinitions"]
# getting the indexes # getting the indexes
global_indexes = body.get("GlobalSecondaryIndexes", [])
table = dynamodb_backend2.create_table(table_name, table = dynamodb_backend2.create_table(table_name,
schema=key_schema, schema=key_schema,
throughput=throughput, throughput=throughput,
attr=attr) attr=attr,
global_indexes=global_indexes)
return dynamo_json_dump(table.describe) return dynamo_json_dump(table.describe)
def delete_table(self): def delete_table(self):
@ -216,13 +218,14 @@ class DynamoHandler(BaseResponse):
def query(self): def query(self):
name = self.body['TableName'] name = self.body['TableName']
keys = self.body['KeyConditions'] key_conditions = self.body['KeyConditions']
hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name) 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: if hash_key_name is None:
er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
return self.error(er) return self.error(er)
hash_key = keys[hash_key_name]['AttributeValueList'][0] hash_key = key_conditions[hash_key_name]['AttributeValueList'][0]
if len(keys) == 1: if len(key_conditions) == 1:
range_comparison = None range_comparison = None
range_values = [] range_values = []
else: else:
@ -230,7 +233,7 @@ class DynamoHandler(BaseResponse):
er = "com.amazon.coral.validate#ValidationException" er = "com.amazon.coral.validate#ValidationException"
return self.error(er) return self.error(er)
else: else:
range_condition = keys[range_key_name] range_condition = key_conditions[range_key_name]
if range_condition: if range_condition:
range_comparison = range_condition['ComparisonOperator'] range_comparison = range_condition['ComparisonOperator']
range_values = range_condition['AttributeValueList'] range_values = range_condition['AttributeValueList']

View File

@ -48,7 +48,7 @@ def test_create_table():
}, },
'TableName': 'messages', 'TableName': 'messages',
'TableSizeBytes': 0, 'TableSizeBytes': 0,
'TableStatus': 'ACTIVE' 'TableStatus': 'ACTIVE',
} }
} }
conn.describe_table('messages').should.equal(expected) conn.describe_table('messages').should.equal(expected)

View File

@ -7,10 +7,8 @@ from moto import mock_dynamodb2
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
from tests.helpers import requires_boto_gte from tests.helpers import requires_boto_gte
try: try:
from boto.dynamodb2.fields import HashKey from boto.dynamodb2.fields import GlobalAllIndex, HashKey, RangeKey
from boto.dynamodb2.fields import RangeKey from boto.dynamodb2.table import Item, Table
from boto.dynamodb2.table import Table
from boto.dynamodb2.table import Item
from boto.dynamodb2.exceptions import ValidationException from boto.dynamodb2.exceptions import ValidationException
except ImportError: except ImportError:
pass pass
@ -53,7 +51,8 @@ def test_create_table():
{'KeyType': 'HASH', 'AttributeName': 'forum_name'}, {'KeyType': 'HASH', 'AttributeName': 'forum_name'},
{'KeyType': 'RANGE', 'AttributeName': 'subject'} {'KeyType': 'RANGE', 'AttributeName': 'subject'}
], ],
'ItemCount': 0, 'CreationDateTime': 1326499200.0 'ItemCount': 0, 'CreationDateTime': 1326499200.0,
'GlobalSecondaryIndexes': [],
} }
} }
table.describe().should.equal(expected) table.describe().should.equal(expected)
@ -445,3 +444,92 @@ def test_get_key_fields():
table = create_table() table = create_table()
kf = table.get_key_fields() kf = table.get_key_fields()
kf.should.equal(['forum_name', 'subject']) 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)

View File

@ -43,7 +43,8 @@ def test_create_table():
'KeySchema': [ 'KeySchema': [
{'KeyType': 'HASH', 'AttributeName': 'forum_name'} {'KeyType': 'HASH', 'AttributeName': 'forum_name'}
], ],
'ItemCount': 0, 'CreationDateTime': 1326499200.0 'ItemCount': 0, 'CreationDateTime': 1326499200.0,
'GlobalSecondaryIndexes': [],
} }
} }
conn = boto.dynamodb2.connect_to_region( conn = boto.dynamodb2.connect_to_region(