diff --git a/moto/dynamodb/models/dynamo_type.py b/moto/dynamodb/models/dynamo_type.py index 28490526d..0adbe95f5 100644 --- a/moto/dynamodb/models/dynamo_type.py +++ b/moto/dynamodb/models/dynamo_type.py @@ -316,14 +316,20 @@ class Item(BaseModel): return sum(bytesize(key) + value.size() for key, value in self.attrs.items()) def to_json(self) -> Dict[str, Any]: - attributes = {} + attributes: Dict[str, Any] = {} for attribute_key, attribute in self.attrs.items(): if isinstance(attribute.value, dict): - attr_value = { + attr_dict_value = { key: value.to_regular_json() for key, value in attribute.value.items() } - attributes[attribute_key] = {attribute.type: attr_value} + attributes[attribute_key] = {attribute.type: attr_dict_value} + elif isinstance(attribute.value, list): + attr_list_value = [ + value.to_regular_json() if isinstance(value, DynamoType) else value + for value in attribute.value + ] + attributes[attribute_key] = {attribute.type: attr_list_value} else: attributes[attribute_key] = {attribute.type: attribute.value} diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 03aeea2f3..9f03efa2c 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -964,12 +964,11 @@ class DynamoHandler(BaseResponse): if len(changed) != len(original): return changed else: - return [ - self._build_updated_new_attributes( - original[index], changed[index] - ) + any_element_has_changed = any( + changed[index] != original[index] for index in range(len(changed)) - ] + ) + return changed if any_element_has_changed else original else: return changed diff --git a/tests/test_dynamodb/test_dynamodb_condition_expressions.py b/tests/test_dynamodb/test_dynamodb_condition_expressions.py index 9f74f2d61..d512f1411 100644 --- a/tests/test_dynamodb/test_dynamodb_condition_expressions.py +++ b/tests/test_dynamodb/test_dynamodb_condition_expressions.py @@ -3,6 +3,7 @@ from decimal import Decimal import boto3 import pytest +from botocore.exceptions import ClientError from moto import mock_aws @@ -421,3 +422,153 @@ def test_condition_expression_with_reserved_keyword_as_attr_name(): item = table.get_item(Key={"id": "key-0"})["Item"] assert item == {"id": "key-0", "first": {}} + + +@mock_aws +def test_condition_check_failure_exception_is_raised_when_values_are_returned_for_an_item_with_a_top_level_list(): + # This explicitly tests for a failure in handling JSONification of DynamoType + # when lists are at the top level of an item. + # This exception should not be raised: + # TypeError: Object of type DynamoType is not JSON serializable + + dynamodb_client = boto3.client("dynamodb", region_name="us-east-1") + dynamodb_client.create_table( + TableName="example_table", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + record = { + "id": {"S": "example_id"}, + "some_list": {"L": [{"M": {"hello": {"S": "h"}}}]}, + } + dynamodb_client.put_item( + TableName="example_table", + Item=record, + ) + + with pytest.raises(ClientError) as error: + dynamodb_client.update_item( + TableName="example_table", + Key={"id": {"S": "example_id"}}, + UpdateExpression="set some_list=list_append(some_list, :w)", + ExpressionAttributeValues={ + ":w": {"L": [{"M": {"world": {"S": "w"}}}]}, + ":id": {"S": "incorrect id"}, + }, + ConditionExpression="id = :id", + ReturnValuesOnConditionCheckFailure="ALL_OLD", + ) + + assert error.type.__name__ == "ConditionalCheckFailedException" + assert error.value.response["Error"] == { + "Message": "The conditional request failed", + "Code": "ConditionalCheckFailedException", + } + assert error.value.response["Item"] == { + "id": {"S": "example_id"}, + "some_list": {"L": [{"M": {"hello": {"S": "h"}}}]}, + } + + +@mock_aws +def test_condition_check_failure_exception_is_raised_when_values_are_returned_for_an_item_with_a_top_level_string_set(): + # This explicitly tests for a failure in handling JSONification of DynamoType + # when string sets are at the top level of an item. + # These exception should not be raised: + # TypeError: Object of type DynamoType is not JSON serializable + # AttributeError: 'str' object has no attribute 'to_regular_json' + + dynamodb_client = boto3.client("dynamodb", region_name="us-east-1") + dynamodb_client.create_table( + TableName="example_table", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + record = { + "id": {"S": "example_id"}, + "some_list": {"SS": ["hello"]}, + } + dynamodb_client.put_item( + TableName="example_table", + Item=record, + ) + + with pytest.raises(ClientError) as error: + dynamodb_client.update_item( + TableName="example_table", + Key={"id": {"S": "example_id"}}, + UpdateExpression="set some_list=list_append(some_list, :w)", + ExpressionAttributeValues={ + ":w": {"SS": ["world"]}, + ":id": {"S": "incorrect id"}, + }, + ConditionExpression="id = :id", + ReturnValuesOnConditionCheckFailure="ALL_OLD", + ) + + assert error.type.__name__ == "ConditionalCheckFailedException" + assert error.value.response["Error"] == { + "Message": "The conditional request failed", + "Code": "ConditionalCheckFailedException", + } + assert error.value.response["Item"] == { + "id": {"S": "example_id"}, + "some_list": {"SS": ["hello"]}, + } + + +@mock_aws +def test_condition_check_failure_exception_is_raised_when_values_are_returned_for_an_item_with_a_list_in_a_map(): + # This explicitly tests for a failure in handling JSONification of DynamoType + # when lists are inside a map + + dynamodb_client = boto3.client("dynamodb", region_name="us-east-1") + dynamodb_client.create_table( + TableName="example_table", + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + ], + BillingMode="PAY_PER_REQUEST", + ) + record = { + "id": {"S": "example_id"}, + "some_list_in_a_map": { + "M": {"some_list": {"L": [{"M": {"hello": {"S": "h"}}}]}} + }, + } + dynamodb_client.put_item( + TableName="example_table", + Item=record, + ) + + with pytest.raises(ClientError) as error: + dynamodb_client.update_item( + TableName="example_table", + Key={"id": {"S": "example_id"}}, + UpdateExpression="set some_list_in_a_map.some_list=list_append(some_list_in_a_map.some_list, :w)", + ExpressionAttributeValues={ + ":w": {"L": [{"M": {"world": {"S": "w"}}}]}, + ":id": {"S": "incorrect id"}, + }, + ConditionExpression="id = :id", + ReturnValuesOnConditionCheckFailure="ALL_OLD", + ) + + assert error.type.__name__ == "ConditionalCheckFailedException" + assert error.value.response["Error"] == { + "Message": "The conditional request failed", + "Code": "ConditionalCheckFailedException", + } + assert error.value.response["Item"] == { + "id": {"S": "example_id"}, + "some_list_in_a_map": { + "M": {"some_list": {"L": [{"M": {"hello": {"S": "h"}}}]}} + }, + }