diff --git a/moto/dynamodb2/exceptions.py b/moto/dynamodb2/exceptions.py index 18e498a90..334cd913a 100644 --- a/moto/dynamodb2/exceptions.py +++ b/moto/dynamodb2/exceptions.py @@ -149,3 +149,18 @@ class IncorrectDataType(MockValidationException): def __init__(self): super(IncorrectDataType, self).__init__(self.inc_data_type_msg) + + +class ConditionalCheckFailed(ValueError): + msg = "The conditional request failed" + + def __init__(self): + super(ConditionalCheckFailed, self).__init__(self.msg) + + +class TransactionCanceledException(ValueError): + cancel_reason_msg = "Transaction cancelled, please refer cancellation reasons for specific reasons [{}]" + + def __init__(self, errors): + msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors])) + super(TransactionCanceledException, self).__init__(msg) diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py index f459cd043..40eefed4e 100644 --- a/moto/dynamodb2/models/__init__.py +++ b/moto/dynamodb2/models/__init__.py @@ -18,6 +18,8 @@ from moto.dynamodb2.exceptions import ( InvalidIndexNameError, ItemSizeTooLarge, ItemSizeToUpdateTooLarge, + ConditionalCheckFailed, + TransactionCanceledException, ) from moto.dynamodb2.models.utilities import bytesize from moto.dynamodb2.models.dynamo_type import DynamoType @@ -459,14 +461,14 @@ class Table(BaseModel): if not overwrite: if not get_expected(expected).expr(current): - raise ValueError("The conditional request failed") + raise ConditionalCheckFailed condition_op = get_filter_expression( condition_expression, expression_attribute_names, expression_attribute_values, ) if not condition_op.expr(current): - raise ValueError("The conditional request failed") + raise ConditionalCheckFailed if range_value: self.items[hash_value][range_value] = item @@ -1076,14 +1078,14 @@ class DynamoDBBackend(BaseBackend): expected = {} if not get_expected(expected).expr(item): - raise ValueError("The conditional request failed") + raise ConditionalCheckFailed condition_op = get_filter_expression( condition_expression, expression_attribute_names, expression_attribute_values, ) if not condition_op.expr(item): - raise ValueError("The conditional request failed") + raise ConditionalCheckFailed # Update does not fail on new items, so create one if item is None: @@ -1136,7 +1138,7 @@ class DynamoDBBackend(BaseBackend): expression_attribute_values, ) if not condition_op.expr(item): - raise ValueError("The conditional request failed") + raise ConditionalCheckFailed return table.delete_item(hash_value, range_value) @@ -1167,8 +1169,9 @@ class DynamoDBBackend(BaseBackend): def transact_write_items(self, transact_items): # Create a backup in case any of the transactions fail original_table_state = copy.deepcopy(self.tables) - try: - for item in transact_items: + errors = [] + for item in transact_items: + try: if "ConditionCheck" in item: item = item["ConditionCheck"] key = item["Key"] @@ -1188,7 +1191,7 @@ class DynamoDBBackend(BaseBackend): expression_attribute_values, ) if not condition_op.expr(current): - raise ValueError("The conditional request failed") + raise ConditionalCheckFailed() elif "Put" in item: item = item["Put"] attrs = item["Item"] @@ -1247,10 +1250,13 @@ class DynamoDBBackend(BaseBackend): ) else: raise ValueError - except: # noqa: E722 Do not use bare except - # Rollback to the original state, and reraise the error + errors.append(None) + except Exception as e: # noqa: E722 Do not use bare except + errors.append(type(e).__name__) + if any(errors): + # Rollback to the original state, and reraise the errors self.tables = original_table_state - raise + raise TransactionCanceledException(errors) def describe_continuous_backups(self, table_name): table = self.get_table(table_name) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 02c4749d3..97c7ee286 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -9,7 +9,12 @@ import six from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores, amzn_request_id -from .exceptions import InvalidIndexNameError, ItemSizeTooLarge, MockValidationException +from .exceptions import ( + InvalidIndexNameError, + ItemSizeTooLarge, + MockValidationException, + TransactionCanceledException, +) from moto.dynamodb2.models import dynamodb_backends, dynamo_json_dump @@ -929,11 +934,9 @@ class DynamoHandler(BaseResponse): transact_items = self.body["TransactItems"] try: self.dynamodb_backend.transact_write_items(transact_items) - except ValueError: - er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException" - return self.error( - er, "A condition specified in the operation could not be evaluated." - ) + except TransactionCanceledException as e: + er = "com.amazonaws.dynamodb.v20111205#TransactionCanceledException" + return self.error(er, str(e)) response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}} return dynamo_json_dump(response) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 8774c3e88..50fd4fd6c 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -4434,13 +4434,8 @@ def test_transact_write_items_put_conditional_expressions(): ] ) # Assert the exception is correct - ex.exception.response["Error"]["Code"].should.equal( - "ConditionalCheckFailedException" - ) + ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException") ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) - ex.exception.response["Error"]["Message"].should.equal( - "A condition specified in the operation could not be evaluated." - ) # Assert all are present items = dynamodb.scan(TableName="test-table")["Items"] items.should.have.length_of(1) @@ -4529,13 +4524,8 @@ def test_transact_write_items_conditioncheck_fails(): ] ) # Assert the exception is correct - ex.exception.response["Error"]["Code"].should.equal( - "ConditionalCheckFailedException" - ) + ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException") ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) - ex.exception.response["Error"]["Message"].should.equal( - "A condition specified in the operation could not be evaluated." - ) # Assert the original email address is still present items = dynamodb.scan(TableName="test-table")["Items"] @@ -4631,13 +4621,8 @@ def test_transact_write_items_delete_with_failed_condition_expression(): ] ) # Assert the exception is correct - ex.exception.response["Error"]["Code"].should.equal( - "ConditionalCheckFailedException" - ) + ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException") ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) - ex.exception.response["Error"]["Message"].should.equal( - "A condition specified in the operation could not be evaluated." - ) # Assert the original item is still present items = dynamodb.scan(TableName="test-table")["Items"] items.should.have.length_of(1) @@ -4709,13 +4694,8 @@ def test_transact_write_items_update_with_failed_condition_expression(): ] ) # Assert the exception is correct - ex.exception.response["Error"]["Code"].should.equal( - "ConditionalCheckFailedException" - ) + ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException") ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) - ex.exception.response["Error"]["Message"].should.equal( - "A condition specified in the operation could not be evaluated." - ) # Assert the original item is still present items = dynamodb.scan(TableName="test-table")["Items"] items.should.have.length_of(1) @@ -5243,3 +5223,48 @@ def test_update_item_add_to_non_existent_number_set(): ) updated_item = ddb_mock.get_item(TableName=table, Key=key)["Item"] assert updated_item["s_i"]["NS"] == ["3"] + + +@mock_dynamodb2 +def test_transact_write_items_fails_with_transaction_canceled_exception(): + table_schema = { + "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}], + "AttributeDefinitions": [{"AttributeName": "id", "AttributeType": "S"},], + } + dynamodb = boto3.client("dynamodb", region_name="us-east-1") + dynamodb.create_table( + TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema + ) + # Insert one item + dynamodb.put_item(TableName="test-table", Item={"id": {"S": "foo"}}) + # Update two items, the one that exists and another that doesn't + with assert_raises(ClientError) as ex: + dynamodb.transact_write_items( + TransactItems=[ + { + "Update": { + "Key": {"id": {"S": "foo"}}, + "TableName": "test-table", + "UpdateExpression": "SET #k = :v", + "ConditionExpression": "attribute_exists(id)", + "ExpressionAttributeNames": {"#k": "key"}, + "ExpressionAttributeValues": {":v": {"S": "value"}}, + } + }, + { + "Update": { + "Key": {"id": {"S": "doesnotexist"}}, + "TableName": "test-table", + "UpdateExpression": "SET #e = :v", + "ConditionExpression": "attribute_exists(id)", + "ExpressionAttributeNames": {"#e": "key"}, + "ExpressionAttributeValues": {":v": {"S": "value"}}, + } + }, + ] + ) + ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException") + ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400) + ex.exception.response["Error"]["Message"].should.equal( + "Transaction cancelled, please refer cancellation reasons for specific reasons [None, ConditionalCheckFailed]" + )