Merge pull request #2986 from bblommers/dynamodb_transact_errors
#2985 - DynamoDB - TransactWriteItems - Fix error-type returned
This commit is contained in:
		
						commit
						9712acc75f
					
				@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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]"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user