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):
|
def __init__(self):
|
||||||
super(IncorrectDataType, self).__init__(self.inc_data_type_msg)
|
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,
|
InvalidIndexNameError,
|
||||||
ItemSizeTooLarge,
|
ItemSizeTooLarge,
|
||||||
ItemSizeToUpdateTooLarge,
|
ItemSizeToUpdateTooLarge,
|
||||||
|
ConditionalCheckFailed,
|
||||||
|
TransactionCanceledException,
|
||||||
)
|
)
|
||||||
from moto.dynamodb2.models.utilities import bytesize
|
from moto.dynamodb2.models.utilities import bytesize
|
||||||
from moto.dynamodb2.models.dynamo_type import DynamoType
|
from moto.dynamodb2.models.dynamo_type import DynamoType
|
||||||
@ -459,14 +461,14 @@ class Table(BaseModel):
|
|||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
if not get_expected(expected).expr(current):
|
if not get_expected(expected).expr(current):
|
||||||
raise ValueError("The conditional request failed")
|
raise ConditionalCheckFailed
|
||||||
condition_op = get_filter_expression(
|
condition_op = get_filter_expression(
|
||||||
condition_expression,
|
condition_expression,
|
||||||
expression_attribute_names,
|
expression_attribute_names,
|
||||||
expression_attribute_values,
|
expression_attribute_values,
|
||||||
)
|
)
|
||||||
if not condition_op.expr(current):
|
if not condition_op.expr(current):
|
||||||
raise ValueError("The conditional request failed")
|
raise ConditionalCheckFailed
|
||||||
|
|
||||||
if range_value:
|
if range_value:
|
||||||
self.items[hash_value][range_value] = item
|
self.items[hash_value][range_value] = item
|
||||||
@ -1076,14 +1078,14 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
expected = {}
|
expected = {}
|
||||||
|
|
||||||
if not get_expected(expected).expr(item):
|
if not get_expected(expected).expr(item):
|
||||||
raise ValueError("The conditional request failed")
|
raise ConditionalCheckFailed
|
||||||
condition_op = get_filter_expression(
|
condition_op = get_filter_expression(
|
||||||
condition_expression,
|
condition_expression,
|
||||||
expression_attribute_names,
|
expression_attribute_names,
|
||||||
expression_attribute_values,
|
expression_attribute_values,
|
||||||
)
|
)
|
||||||
if not condition_op.expr(item):
|
if not condition_op.expr(item):
|
||||||
raise ValueError("The conditional request failed")
|
raise ConditionalCheckFailed
|
||||||
|
|
||||||
# Update does not fail on new items, so create one
|
# Update does not fail on new items, so create one
|
||||||
if item is None:
|
if item is None:
|
||||||
@ -1136,7 +1138,7 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
expression_attribute_values,
|
expression_attribute_values,
|
||||||
)
|
)
|
||||||
if not condition_op.expr(item):
|
if not condition_op.expr(item):
|
||||||
raise ValueError("The conditional request failed")
|
raise ConditionalCheckFailed
|
||||||
|
|
||||||
return table.delete_item(hash_value, range_value)
|
return table.delete_item(hash_value, range_value)
|
||||||
|
|
||||||
@ -1167,8 +1169,9 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
def transact_write_items(self, transact_items):
|
def transact_write_items(self, transact_items):
|
||||||
# Create a backup in case any of the transactions fail
|
# Create a backup in case any of the transactions fail
|
||||||
original_table_state = copy.deepcopy(self.tables)
|
original_table_state = copy.deepcopy(self.tables)
|
||||||
try:
|
errors = []
|
||||||
for item in transact_items:
|
for item in transact_items:
|
||||||
|
try:
|
||||||
if "ConditionCheck" in item:
|
if "ConditionCheck" in item:
|
||||||
item = item["ConditionCheck"]
|
item = item["ConditionCheck"]
|
||||||
key = item["Key"]
|
key = item["Key"]
|
||||||
@ -1188,7 +1191,7 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
expression_attribute_values,
|
expression_attribute_values,
|
||||||
)
|
)
|
||||||
if not condition_op.expr(current):
|
if not condition_op.expr(current):
|
||||||
raise ValueError("The conditional request failed")
|
raise ConditionalCheckFailed()
|
||||||
elif "Put" in item:
|
elif "Put" in item:
|
||||||
item = item["Put"]
|
item = item["Put"]
|
||||||
attrs = item["Item"]
|
attrs = item["Item"]
|
||||||
@ -1247,10 +1250,13 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
except: # noqa: E722 Do not use bare except
|
errors.append(None)
|
||||||
# Rollback to the original state, and reraise the error
|
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
|
self.tables = original_table_state
|
||||||
raise
|
raise TransactionCanceledException(errors)
|
||||||
|
|
||||||
def describe_continuous_backups(self, table_name):
|
def describe_continuous_backups(self, table_name):
|
||||||
table = self.get_table(table_name)
|
table = self.get_table(table_name)
|
||||||
|
@ -9,7 +9,12 @@ import six
|
|||||||
|
|
||||||
from moto.core.responses import BaseResponse
|
from moto.core.responses import BaseResponse
|
||||||
from moto.core.utils import camelcase_to_underscores, amzn_request_id
|
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
|
from moto.dynamodb2.models import dynamodb_backends, dynamo_json_dump
|
||||||
|
|
||||||
|
|
||||||
@ -929,11 +934,9 @@ class DynamoHandler(BaseResponse):
|
|||||||
transact_items = self.body["TransactItems"]
|
transact_items = self.body["TransactItems"]
|
||||||
try:
|
try:
|
||||||
self.dynamodb_backend.transact_write_items(transact_items)
|
self.dynamodb_backend.transact_write_items(transact_items)
|
||||||
except ValueError:
|
except TransactionCanceledException as e:
|
||||||
er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException"
|
er = "com.amazonaws.dynamodb.v20111205#TransactionCanceledException"
|
||||||
return self.error(
|
return self.error(er, str(e))
|
||||||
er, "A condition specified in the operation could not be evaluated."
|
|
||||||
)
|
|
||||||
response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}}
|
response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}}
|
||||||
return dynamo_json_dump(response)
|
return dynamo_json_dump(response)
|
||||||
|
|
||||||
|
@ -4434,13 +4434,8 @@ def test_transact_write_items_put_conditional_expressions():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
# Assert the exception is correct
|
# Assert the exception is correct
|
||||||
ex.exception.response["Error"]["Code"].should.equal(
|
ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException")
|
||||||
"ConditionalCheckFailedException"
|
|
||||||
)
|
|
||||||
ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
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
|
# Assert all are present
|
||||||
items = dynamodb.scan(TableName="test-table")["Items"]
|
items = dynamodb.scan(TableName="test-table")["Items"]
|
||||||
items.should.have.length_of(1)
|
items.should.have.length_of(1)
|
||||||
@ -4529,13 +4524,8 @@ def test_transact_write_items_conditioncheck_fails():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
# Assert the exception is correct
|
# Assert the exception is correct
|
||||||
ex.exception.response["Error"]["Code"].should.equal(
|
ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException")
|
||||||
"ConditionalCheckFailedException"
|
|
||||||
)
|
|
||||||
ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
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
|
# Assert the original email address is still present
|
||||||
items = dynamodb.scan(TableName="test-table")["Items"]
|
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
|
# Assert the exception is correct
|
||||||
ex.exception.response["Error"]["Code"].should.equal(
|
ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException")
|
||||||
"ConditionalCheckFailedException"
|
|
||||||
)
|
|
||||||
ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
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
|
# Assert the original item is still present
|
||||||
items = dynamodb.scan(TableName="test-table")["Items"]
|
items = dynamodb.scan(TableName="test-table")["Items"]
|
||||||
items.should.have.length_of(1)
|
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
|
# Assert the exception is correct
|
||||||
ex.exception.response["Error"]["Code"].should.equal(
|
ex.exception.response["Error"]["Code"].should.equal("TransactionCanceledException")
|
||||||
"ConditionalCheckFailedException"
|
|
||||||
)
|
|
||||||
ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
|
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
|
# Assert the original item is still present
|
||||||
items = dynamodb.scan(TableName="test-table")["Items"]
|
items = dynamodb.scan(TableName="test-table")["Items"]
|
||||||
items.should.have.length_of(1)
|
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"]
|
updated_item = ddb_mock.get_item(TableName=table, Key=key)["Item"]
|
||||||
assert updated_item["s_i"]["NS"] == ["3"]
|
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…
Reference in New Issue
Block a user