From be07fbda523592c4d29b30453e1f182a9dce630a Mon Sep 17 00:00:00 2001 From: Greg Sterin Date: Fri, 9 Jun 2017 17:32:19 -0700 Subject: [PATCH] Support Expected in dynamoDB updateItem --- moto/dynamodb2/models.py | 31 ++++++++++- moto/dynamodb2/responses.py | 38 ++++++++++++- .../test_dynamodb_table_without_range_key.py | 55 +++++++++++++++++++ 3 files changed, 120 insertions(+), 4 deletions(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index d632119d9..7525a43a9 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -634,7 +634,8 @@ class DynamoDBBackend(BaseBackend): return table.scan(scan_filters, limit, exclusive_start_key) - def update_item(self, table_name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values): + def update_item(self, table_name, key, update_expression, attribute_updates, expression_attribute_names, + expression_attribute_values, expected=None): table = self.get_table(table_name) if all([table.hash_key_attr in key, table.range_key_attr in key]): @@ -652,6 +653,34 @@ class DynamoDBBackend(BaseBackend): range_value = None item = table.get_item(hash_value, range_value) + + if item is None: + item_attr = {} + elif hasattr(item, 'attrs'): + item_attr = item.attrs + else: + item_attr = item + + if not expected: + expected = {} + + for key, val in expected.items(): + if 'Exists' in val and val['Exists'] is False: + if key in item_attr: + raise ValueError("The conditional request failed") + elif key not in item_attr: + raise ValueError("The conditional request failed") + elif 'Value' in val and DynamoType(val['Value']).value != item_attr[key].value: + raise ValueError("The conditional request failed") + elif 'ComparisonOperator' in val: + comparison_func = get_comparison_func( + val['ComparisonOperator']) + dynamo_types = [DynamoType(ele) for ele in val[ + "AttributeValueList"]] + for t in dynamo_types: + if not comparison_func(item_attr[key].value, t.value): + raise ValueError('The conditional request failed') + # Update does not fail on new items, so create one if item is None: data = { diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index aa5561f58..1d9b70043 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -207,7 +207,7 @@ class DynamoHandler(BaseResponse): try: result = dynamodb_backend2.put_item( name, item, expected, overwrite) - except Exception: + except ValueError: er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' return self.error(er) @@ -474,14 +474,46 @@ class DynamoHandler(BaseResponse): 'ExpressionAttributeValues', {}) existing_item = dynamodb_backend2.get_item(name, key) + if 'Expected' in self.body: + expected = self.body['Expected'] + else: + expected = None + + # Attempt to parse simple ConditionExpressions into an Expected + # expression + if not expected: + condition_expression = self.body.get('ConditionExpression') + if condition_expression and 'OR' not in condition_expression: + cond_items = [c.strip() + for c in condition_expression.split('AND')] + + if cond_items: + expected = {} + exists_re = re.compile('^attribute_exists\((.*)\)$') + not_exists_re = re.compile( + '^attribute_not_exists\((.*)\)$') + + for cond in cond_items: + exists_m = exists_re.match(cond) + not_exists_m = not_exists_re.match(cond) + if exists_m: + expected[exists_m.group(1)] = {'Exists': True} + elif not_exists_m: + expected[not_exists_m.group(1)] = {'Exists': False} + # Support spaces between operators in an update expression # E.g. `a = b + c` -> `a=b+c` if update_expression: update_expression = re.sub( '\s*([=\+-])\s*', '\\1', update_expression) - item = dynamodb_backend2.update_item( - name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values) + try: + item = dynamodb_backend2.update_item( + name, key, update_expression, attribute_updates, expression_attribute_names, expression_attribute_values, + expected) + except ValueError: + er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' + return self.error(er) item_dict = item.to_json() item_dict['ConsumedCapacityUnits'] = 0.5 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 4f08c5094..0e1099559 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -608,6 +608,61 @@ def test_boto3_put_item_conditions_fails(): } }).should.throw(botocore.client.ClientError) +@mock_dynamodb2 +def test_boto3_update_item_conditions_fails(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'baz'}) + table.update_item.when.called_with( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=bar', + Expected={ + 'foo': { + 'Value': 'bar', + } + }).should.throw(botocore.client.ClientError) + +@mock_dynamodb2 +def test_boto3_update_item_conditions_fails_because_expect_not_exists(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'baz'}) + table.update_item.when.called_with( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=bar', + Expected={ + 'foo': { + 'Exists': False + } + }).should.throw(botocore.client.ClientError) + +@mock_dynamodb2 +def test_boto3_update_item_conditions_pass(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'bar'}) + table.update_item( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=baz', + Expected={ + 'foo': { + 'Value': 'bar', + } + }) + returned_item = table.get_item(Key={'username': 'johndoe'}) + assert dict(returned_item)['Item']['foo'].should.equal("baz") + +@mock_dynamodb2 +def test_boto3_update_item_conditions_pass_because_expext_not_exists(): + table = _create_user_table() + table.put_item(Item={'username': 'johndoe', 'foo': 'bar'}) + table.update_item( + Key={'username': 'johndoe'}, + UpdateExpression='SET foo=baz', + Expected={ + 'whatever': { + 'Exists': False, + } + }) + returned_item = table.get_item(Key={'username': 'johndoe'}) + assert dict(returned_item)['Item']['foo'].should.equal("baz") @mock_dynamodb2 def test_boto3_put_item_conditions_pass():