From b74625db0cf6676bd57cd09e610a202fe176117d Mon Sep 17 00:00:00 2001 From: Tomoya Iwata Date: Sun, 13 Jan 2019 17:38:38 +0900 Subject: [PATCH] add support for dynamodb transact_get_items --- IMPLEMENTATION_COVERAGE.md | 4 +- moto/dynamodb2/responses.py | 69 ++++++ tests/test_dynamodb2/test_dynamodb.py | 308 +++++++++++++++++++++++++- 3 files changed, 378 insertions(+), 3 deletions(-) diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index a22cc3bfb..705618524 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -2237,7 +2237,7 @@ - [ ] verify_trust ## dynamodb -17% implemented +24% implemented - [ ] batch_get_item - [ ] batch_write_item - [ ] create_backup @@ -2268,7 +2268,7 @@ - [ ] restore_table_to_point_in_time - [X] scan - [ ] tag_resource -- [ ] transact_get_items +- [X] transact_get_items - [ ] transact_write_items - [ ] untag_resource - [ ] update_continuous_backups diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index d3767c3fd..c9b526121 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -10,6 +10,9 @@ from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSize from .models import dynamodb_backends, dynamo_json_dump +TRANSACTION_MAX_ITEMS = 10 + + def has_empty_keys_or_values(_dict): if _dict == "": return True @@ -828,3 +831,69 @@ class DynamoHandler(BaseResponse): ttl_spec = self.dynamodb_backend.describe_ttl(name) return json.dumps({"TimeToLiveDescription": ttl_spec}) + + def transact_get_items(self): + transact_items = self.body['TransactItems'] + responses = list() + + if len(transact_items) > TRANSACTION_MAX_ITEMS: + msg = "1 validation error detected: Value '[" + err_list = list() + request_id = 268435456 + for _ in transact_items: + request_id += 1 + hex_request_id = format(request_id, 'x') + err_list.append('com.amazonaws.dynamodb.v20120810.TransactGetItem@%s' % hex_request_id) + msg += ', '.join(err_list) + msg += "'] at 'transactItems' failed to satisfy constraint: " \ + "Member must have length less than or equal to %s" % TRANSACTION_MAX_ITEMS + + return self.error('ValidationException', msg) + + dedup_list = [i for n, i in enumerate(transact_items) if i not in transact_items[n + 1:]] + if len(transact_items) != len(dedup_list): + er = 'com.amazon.coral.validate#ValidationException' + return self.error(er, 'Transaction request cannot include multiple operations on one item') + + ret_consumed_capacity = self.body.get('ReturnConsumedCapacity', 'NONE') + consumed_capacity = dict() + + for transact_item in transact_items: + + table_name = transact_item['Get']['TableName'] + key = transact_item['Get']['Key'] + try: + item = self.dynamodb_backend.get_item(table_name, key) + except ValueError as e: + er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' + return self.error(er, 'Requested resource not found') + + if not item: + continue + + item_describe = item.describe_attrs(False) + responses.append(item_describe) + + table_capacity = consumed_capacity.get(table_name, {}) + table_capacity['TableName'] = table_name + capacity_units = table_capacity.get('CapacityUnits', 0) + 2.0 + table_capacity['CapacityUnits'] = capacity_units + read_capacity_units = table_capacity.get('ReadCapacityUnits', 0) + 2.0 + table_capacity['ReadCapacityUnits'] = read_capacity_units + consumed_capacity[table_name] = table_capacity + + if ret_consumed_capacity == 'INDEXES': + table_capacity['Table'] = { + 'CapacityUnits': capacity_units, + 'ReadCapacityUnits': read_capacity_units + } + + result = dict() + result.update({ + 'Responses': responses}) + if ret_consumed_capacity != 'NONE': + result.update({ + 'ConsumedCapacity': [v for v in consumed_capacity.values()] + }) + + return dynamo_json_dump(result) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 428b58f81..e439eeeb9 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -6,8 +6,9 @@ import six import boto import boto3 from boto3.dynamodb.conditions import Attr, Key -import sure # noqa +import re import requests +import sure # noqa from moto import mock_dynamodb2, mock_dynamodb2_deprecated from moto.dynamodb2 import dynamodb_backend2, dynamodb_backends2 from boto.exception import JSONResponseError @@ -3792,3 +3793,308 @@ def test_query_catches_when_no_filters(): ex.exception.response["Error"]["Message"].should.equal( "Either KeyConditions or QueryFilter should be present" ) + + +@mock_dynamodb2 +def test_invalid_transact_get_items(): + + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb.create_table( + TableName='test1', + KeySchema=[{'AttributeName': 'id', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'id', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5} + ) + table = dynamodb.Table('test1') + table.put_item(Item={ + 'id': '1', + 'val': '1', + }) + + table.put_item(Item={ + 'id': '1', + 'val': '2', + }) + + client = boto3.client('dynamodb', region_name='us-east-1') + + with assert_raises(ClientError) as ex: + client.transact_get_items(TransactItems=[ + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + }, + 'TableName': 'test1' + } + } + ]) + + ex.exception.response['Error']['Code'].should.equal('ValidationException') + ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal( + 'Transaction request cannot include multiple operations on one item' + ) + + with assert_raises(ClientError) as ex: + client.transact_get_items(TransactItems=[ + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + {'Get': {'Key': {'id': {'S': '1'}}, 'TableName': 'test1'}}, + ]) + + ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.match( + r'failed to satisfy constraint: Member must have length less than or equal to 10', re.I + ) + + with assert_raises(ClientError) as ex: + client.transact_get_items(TransactItems=[ + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + }, + 'TableName': 'non_exists_table' + } + } + ]) + + ex.exception.response['Error']['Code'].should.equal('ResourceNotFoundException') + ex.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + ex.exception.response['Error']['Message'].should.equal( + 'Requested resource not found' + ) + + +@mock_dynamodb2 +def test_valid_transact_get_items(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + dynamodb.create_table( + TableName='test1', + KeySchema=[ + {'AttributeName': 'id', 'KeyType': 'HASH'}, + {'AttributeName': 'sort_key', 'KeyType': 'RANGE'}, + ], + AttributeDefinitions=[ + {'AttributeName': 'id', 'AttributeType': 'S'}, + {'AttributeName': 'sort_key', 'AttributeType': 'S'}, + ], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5} + ) + table1 = dynamodb.Table('test1') + table1.put_item(Item={ + 'id': '1', + 'sort_key': '1', + }) + + table1.put_item(Item={ + 'id': '1', + 'sort_key': '2', + }) + + dynamodb.create_table( + TableName='test2', + KeySchema=[ + {'AttributeName': 'id', 'KeyType': 'HASH'}, + {'AttributeName': 'sort_key', 'KeyType': 'RANGE'}, + ], + AttributeDefinitions=[ + {'AttributeName': 'id', 'AttributeType': 'S'}, + {'AttributeName': 'sort_key', 'AttributeType': 'S'}, + ], + ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5} + ) + table2 = dynamodb.Table('test2') + table2.put_item(Item={ + 'id': '1', + 'sort_key': '1', + }) + + client = boto3.client('dynamodb', region_name='us-east-1') + res = client.transact_get_items(TransactItems=[ + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': 'non_exists_key'}, + 'sort_key': {'S': '2'} + }, + 'TableName': 'test1' + } + } + ]) + res['Responses'][0]['Item'].should.equal({ + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }) + len(res['Responses']).should.equal(1) + + res = client.transact_get_items(TransactItems=[ + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '2'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test2' + } + }, + ]) + + res['Responses'][0]['Item'].should.equal({ + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }) + + res['Responses'][1]['Item'].should.equal({ + 'id': {'S': '1'}, + 'sort_key': {'S': '2'} + }) + + res['Responses'][2]['Item'].should.equal({ + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }) + + res = client.transact_get_items(TransactItems=[ + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '2'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test2' + } + }, + ], ReturnConsumedCapacity='TOTAL') + + res['ConsumedCapacity'][0].should.equal({ + 'TableName': 'test1', + 'CapacityUnits': 4.0, + 'ReadCapacityUnits': 4.0 + }) + + res['ConsumedCapacity'][1].should.equal({ + 'TableName': 'test2', + 'CapacityUnits': 2.0, + 'ReadCapacityUnits': 2.0 + }) + + res = client.transact_get_items(TransactItems=[ + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '2'} + }, + 'TableName': 'test1' + } + }, + { + 'Get': { + 'Key': { + 'id': {'S': '1'}, + 'sort_key': {'S': '1'} + }, + 'TableName': 'test2' + } + }, + ], ReturnConsumedCapacity='INDEXES') + + res['ConsumedCapacity'][0].should.equal({ + 'TableName': 'test1', + 'CapacityUnits': 4.0, + 'ReadCapacityUnits': 4.0, + 'Table': { + 'CapacityUnits': 4.0, + 'ReadCapacityUnits': 4.0, + } + }) + + res['ConsumedCapacity'][1].should.equal({ + 'TableName': 'test2', + 'CapacityUnits': 2.0, + 'ReadCapacityUnits': 2.0, + 'Table': { + 'CapacityUnits': 2.0, + 'ReadCapacityUnits': 2.0, + } + })