DynamoDb2 transact_write_items: Check multiple transacts on same item (#4787)
This commit is contained in:
parent
6d160303a4
commit
44e01a298e
@ -198,6 +198,13 @@ class TransactionCanceledException(ValueError):
|
|||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class MultipleTransactionsException(MockValidationException):
|
||||||
|
msg = "Transaction request cannot include multiple operations on one item"
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(self.msg)
|
||||||
|
|
||||||
|
|
||||||
class EmptyKeyAttributeException(MockValidationException):
|
class EmptyKeyAttributeException(MockValidationException):
|
||||||
empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string"
|
empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string"
|
||||||
# AWS has a different message for empty index keys
|
# AWS has a different message for empty index keys
|
||||||
|
@ -23,6 +23,7 @@ from moto.dynamodb2.exceptions import (
|
|||||||
TransactionCanceledException,
|
TransactionCanceledException,
|
||||||
EmptyKeyAttributeException,
|
EmptyKeyAttributeException,
|
||||||
InvalidAttributeTypeError,
|
InvalidAttributeTypeError,
|
||||||
|
MultipleTransactionsException,
|
||||||
)
|
)
|
||||||
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
|
||||||
@ -1566,6 +1567,14 @@ 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)
|
||||||
|
target_items = set()
|
||||||
|
|
||||||
|
def check_unicity(table_name, key):
|
||||||
|
item = (str(table_name), str(key))
|
||||||
|
if item in target_items:
|
||||||
|
raise MultipleTransactionsException()
|
||||||
|
target_items.add(item)
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for item in transact_items:
|
for item in transact_items:
|
||||||
try:
|
try:
|
||||||
@ -1573,6 +1582,7 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
item = item["ConditionCheck"]
|
item = item["ConditionCheck"]
|
||||||
key = item["Key"]
|
key = item["Key"]
|
||||||
table_name = item["TableName"]
|
table_name = item["TableName"]
|
||||||
|
check_unicity(table_name, key)
|
||||||
condition_expression = item.get("ConditionExpression", None)
|
condition_expression = item.get("ConditionExpression", None)
|
||||||
expression_attribute_names = item.get(
|
expression_attribute_names = item.get(
|
||||||
"ExpressionAttributeNames", None
|
"ExpressionAttributeNames", None
|
||||||
@ -1611,6 +1621,7 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
item = item["Delete"]
|
item = item["Delete"]
|
||||||
key = item["Key"]
|
key = item["Key"]
|
||||||
table_name = item["TableName"]
|
table_name = item["TableName"]
|
||||||
|
check_unicity(table_name, key)
|
||||||
condition_expression = item.get("ConditionExpression", None)
|
condition_expression = item.get("ConditionExpression", None)
|
||||||
expression_attribute_names = item.get(
|
expression_attribute_names = item.get(
|
||||||
"ExpressionAttributeNames", None
|
"ExpressionAttributeNames", None
|
||||||
@ -1629,6 +1640,7 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
item = item["Update"]
|
item = item["Update"]
|
||||||
key = item["Key"]
|
key = item["Key"]
|
||||||
table_name = item["TableName"]
|
table_name = item["TableName"]
|
||||||
|
check_unicity(table_name, key)
|
||||||
update_expression = item["UpdateExpression"]
|
update_expression = item["UpdateExpression"]
|
||||||
condition_expression = item.get("ConditionExpression", None)
|
condition_expression = item.get("ConditionExpression", None)
|
||||||
expression_attribute_names = item.get(
|
expression_attribute_names = item.get(
|
||||||
@ -1648,6 +1660,10 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
else:
|
else:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
errors.append(None)
|
errors.append(None)
|
||||||
|
except MultipleTransactionsException:
|
||||||
|
# Rollback to the original state, and reraise the error
|
||||||
|
self.tables = original_table_state
|
||||||
|
raise MultipleTransactionsException()
|
||||||
except Exception as e: # noqa: E722 Do not use bare except
|
except Exception as e: # noqa: E722 Do not use bare except
|
||||||
errors.append(type(e).__name__)
|
errors.append(type(e).__name__)
|
||||||
if any(errors):
|
if any(errors):
|
||||||
|
@ -1127,6 +1127,9 @@ class DynamoHandler(BaseResponse):
|
|||||||
except TransactionCanceledException as e:
|
except TransactionCanceledException as e:
|
||||||
er = "com.amazonaws.dynamodb.v20111205#TransactionCanceledException"
|
er = "com.amazonaws.dynamodb.v20111205#TransactionCanceledException"
|
||||||
return self.error(er, str(e))
|
return self.error(er, str(e))
|
||||||
|
except MockValidationException as mve:
|
||||||
|
er = "com.amazonaws.dynamodb.v20111205#ValidationException"
|
||||||
|
return self.error(er, mve.exception_msg)
|
||||||
response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}}
|
response = {"ConsumedCapacity": [], "ItemCollectionMetrics": {}}
|
||||||
return dynamo_json_dump(response)
|
return dynamo_json_dump(response)
|
||||||
|
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import boto3
|
import boto3
|
||||||
import pytest
|
import pytest
|
||||||
import sure # noqa # pylint: disable=unused-import
|
import sure # noqa # pylint: disable=unused-import
|
||||||
|
|
||||||
from botocore.exceptions import ClientError
|
|
||||||
from boto3.dynamodb.conditions import Key
|
from boto3.dynamodb.conditions import Key
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
from moto import mock_dynamodb2
|
from moto import mock_dynamodb2
|
||||||
|
|
||||||
|
|
||||||
table_schema = {
|
table_schema = {
|
||||||
"KeySchema": [{"AttributeName": "partitionKey", "KeyType": "HASH"}],
|
"KeySchema": [{"AttributeName": "partitionKey", "KeyType": "HASH"}],
|
||||||
"GlobalSecondaryIndexes": [
|
"GlobalSecondaryIndexes": [
|
||||||
@ -466,3 +464,41 @@ def test_creating_table_with_0_global_indexes():
|
|||||||
err["Message"].should.equal(
|
err["Message"].should.equal(
|
||||||
"One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
|
"One or more parameter values were invalid: List of GlobalSecondaryIndexes is empty"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_dynamodb2
|
||||||
|
def test_multiple_transactions_on_same_item():
|
||||||
|
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 an item
|
||||||
|
dynamodb.put_item(TableName="test-table", Item={"id": {"S": "foo"}})
|
||||||
|
|
||||||
|
def update_email_transact(email):
|
||||||
|
return {
|
||||||
|
"Update": {
|
||||||
|
"Key": {"id": {"S": "foo"}},
|
||||||
|
"TableName": "test-table",
|
||||||
|
"UpdateExpression": "SET #e = :v",
|
||||||
|
"ExpressionAttributeNames": {"#e": "email_address"},
|
||||||
|
"ExpressionAttributeValues": {":v": {"S": email}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(ClientError) as exc:
|
||||||
|
dynamodb.transact_write_items(
|
||||||
|
TransactItems=[
|
||||||
|
update_email_transact("test1@moto.com"),
|
||||||
|
update_email_transact("test2@moto.com"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
err = exc.value.response["Error"]
|
||||||
|
err["Code"].should.equal("ValidationException")
|
||||||
|
err["Message"].should.equal(
|
||||||
|
"Transaction request cannot include multiple operations on one item"
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user