Merge pull request #2509 from bblommers/feature/847

DynamoDB - Support list_append operation
This commit is contained in:
Jack Danger 2019-10-23 15:20:37 -07:00 committed by GitHub
commit 61f153f168
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 94 additions and 9 deletions

View File

@ -9,6 +9,7 @@ import uuid
import six import six
import boto3 import boto3
from botocore.exceptions import ParamValidationError
from moto.compat import OrderedDict from moto.compat import OrderedDict
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
from moto.core.utils import unix_time from moto.core.utils import unix_time
@ -302,6 +303,8 @@ class Item(BaseModel):
attr, list_index = attribute_is_list(key.split('.')[0]) attr, list_index = attribute_is_list(key.split('.')[0])
# If value not exists, changes value to a default if needed, else its the same as it was # If value not exists, changes value to a default if needed, else its the same as it was
value = self._get_default(value) value = self._get_default(value)
# If operation == list_append, get the original value and append it
value = self._get_appended_list(value, expression_attribute_values)
if type(value) != DynamoType: if type(value) != DynamoType:
if value in expression_attribute_values: if value in expression_attribute_values:
@ -370,6 +373,18 @@ class Item(BaseModel):
else: else:
raise NotImplementedError('{} update action not yet supported'.format(action)) raise NotImplementedError('{} update action not yet supported'.format(action))
def _get_appended_list(self, value, expression_attribute_values):
if type(value) != DynamoType:
list_append_re = re.match('list_append\\((.+),(.+)\\)', value)
if list_append_re:
new_value = expression_attribute_values[list_append_re.group(2).strip()]
old_list = self.attrs[list_append_re.group(1)]
if not old_list.is_list():
raise ParamValidationError
old_list.value.extend(new_value['L'])
value = old_list
return value
def _get_default(self, value): def _get_default(self, value):
if value.startswith('if_not_exists'): if value.startswith('if_not_exists'):
# Function signature # Function signature

View File

@ -11,7 +11,7 @@ import requests
from moto import mock_dynamodb2, mock_dynamodb2_deprecated from moto import mock_dynamodb2, mock_dynamodb2_deprecated
from moto.dynamodb2 import dynamodb_backend2 from moto.dynamodb2 import dynamodb_backend2
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, ParamValidationError
from tests.helpers import requires_boto_gte from tests.helpers import requires_boto_gte
import tests.backport_assert_raises import tests.backport_assert_raises
@ -1323,7 +1323,7 @@ def test_bad_scan_filter():
except ClientError as err: except ClientError as err:
err.response['Error']['Code'].should.equal('ValidationError') err.response['Error']['Code'].should.equal('ValidationError')
else: else:
raise RuntimeError('Should of raised ResourceInUseException') raise RuntimeError('Should have raised ResourceInUseException')
@mock_dynamodb2 @mock_dynamodb2
@ -1375,7 +1375,7 @@ def test_duplicate_create():
except ClientError as err: except ClientError as err:
err.response['Error']['Code'].should.equal('ResourceInUseException') err.response['Error']['Code'].should.equal('ResourceInUseException')
else: else:
raise RuntimeError('Should of raised ResourceInUseException') raise RuntimeError('Should have raised ResourceInUseException')
@mock_dynamodb2 @mock_dynamodb2
@ -1400,7 +1400,7 @@ def test_delete_table():
except ClientError as err: except ClientError as err:
err.response['Error']['Code'].should.equal('ResourceNotFoundException') err.response['Error']['Code'].should.equal('ResourceNotFoundException')
else: else:
raise RuntimeError('Should of raised ResourceNotFoundException') raise RuntimeError('Should have raised ResourceNotFoundException')
@mock_dynamodb2 @mock_dynamodb2
@ -2734,6 +2734,13 @@ def test_item_size_is_under_400KB():
Item={'id': {'S': 'foo'}, 'itemlist': {'L': [{'M': {'item1': {'S': large_item}}}]}}) Item={'id': {'S': 'foo'}, 'itemlist': {'L': [{'M': {'item1': {'S': large_item}}}]}})
def assert_failure_due_to_item_size(func, **kwargs):
with assert_raises(ClientError) as ex:
func(**kwargs)
ex.exception.response['Error']['Code'].should.equal('ValidationException')
ex.exception.response['Error']['Message'].should.equal('Item size has exceeded the maximum allowed size')
@mock_dynamodb2 @mock_dynamodb2
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression
def test_hash_key_cannot_use_begins_with_operations(): def test_hash_key_cannot_use_begins_with_operations():
@ -2759,11 +2766,74 @@ def test_hash_key_cannot_use_begins_with_operations():
ex.exception.response['Error']['Message'].should.equal('Query key condition not supported') ex.exception.response['Error']['Message'].should.equal('Query key condition not supported')
def assert_failure_due_to_item_size(func, **kwargs): @mock_dynamodb2
with assert_raises(ClientError) as ex: def test_update_supports_complex_expression_attribute_values():
func(**kwargs) client = boto3.client('dynamodb')
ex.exception.response['Error']['Code'].should.equal('ValidationException')
ex.exception.response['Error']['Message'].should.equal('Item size has exceeded the maximum allowed size') client.create_table(AttributeDefinitions=[{'AttributeName': 'SHA256', 'AttributeType': 'S'}],
TableName='TestTable',
KeySchema=[{'AttributeName': 'SHA256', 'KeyType': 'HASH'}],
ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5})
client.update_item(TableName='TestTable',
Key={'SHA256': {'S': 'sha-of-file'}},
UpdateExpression=('SET MD5 = :md5,'
'MyStringSet = :string_set,'
'MyMap = :map' ),
ExpressionAttributeValues={':md5': {'S': 'md5-of-file'},
':string_set': {'SS': ['string1', 'string2']},
':map': {'M': {'EntryKey': {'SS': ['thing1', 'thing2']}}}})
result = client.get_item(TableName='TestTable', Key={'SHA256': {'S': 'sha-of-file'}})['Item']
result.should.equal({u'MyStringSet': {u'SS': [u'string1', u'string2']},
'MyMap': {u'M': {u'EntryKey': {u'SS': [u'thing1', u'thing2']}}},
'SHA256': {u'S': u'sha-of-file'},
'MD5': {u'S': u'md5-of-file'}})
@mock_dynamodb2
def test_update_supports_list_append():
client = boto3.client('dynamodb')
client.create_table(AttributeDefinitions=[{'AttributeName': 'SHA256', 'AttributeType': 'S'}],
TableName='TestTable',
KeySchema=[{'AttributeName': 'SHA256', 'KeyType': 'HASH'}],
ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5})
client.put_item(TableName='TestTable',
Item={'SHA256': {'S': 'sha-of-file'}, 'crontab': {'L': [{'S': 'bar1'}]}})
# Update item using list_append expression
client.update_item(TableName='TestTable',
Key={'SHA256': {'S': 'sha-of-file'}},
UpdateExpression="SET crontab = list_append(crontab, :i)",
ExpressionAttributeValues={':i': {'L': [{'S': 'bar2'}]}})
# Verify item is appended to the existing list
result = client.get_item(TableName='TestTable', Key={'SHA256': {'S': 'sha-of-file'}})['Item']
result.should.equal({'SHA256': {'S': 'sha-of-file'},
'crontab': {'L': [{'S': 'bar1'}, {'S': 'bar2'}]}})
@mock_dynamodb2
def test_update_catches_invalid_list_append_operation():
client = boto3.client('dynamodb')
client.create_table(AttributeDefinitions=[{'AttributeName': 'SHA256', 'AttributeType': 'S'}],
TableName='TestTable',
KeySchema=[{'AttributeName': 'SHA256', 'KeyType': 'HASH'}],
ProvisionedThroughput={'ReadCapacityUnits': 5, 'WriteCapacityUnits': 5})
client.put_item(TableName='TestTable',
Item={'SHA256': {'S': 'sha-of-file'}, 'crontab': {'L': [{'S': 'bar1'}]}})
# Update item using invalid list_append expression
with assert_raises(ParamValidationError) as ex:
client.update_item(TableName='TestTable',
Key={'SHA256': {'S': 'sha-of-file'}},
UpdateExpression="SET crontab = list_append(crontab, :i)",
ExpressionAttributeValues={':i': [{'S': 'bar2'}]})
# Verify correct error is returned
str(ex.exception).should.match("Parameter validation failed:")
str(ex.exception).should.match("Invalid type for parameter ExpressionAttributeValues.")
def _create_user_table(): def _create_user_table():