diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 1527821ed..52f336851 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -146,6 +146,9 @@ class DynamoType(object): def __eq__(self, other): return self.type == other.type and self.value == other.value + def __ne__(self, other): + return self.type != other.type or self.value != other.value + def __lt__(self, other): return self.cast_value < other.cast_value diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 3ccd161b9..c72ded2c3 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -1,9 +1,12 @@ from __future__ import unicode_literals -import itertools + +import copy import json -import six import re +import itertools +import six + from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores, amzn_request_id from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSizeTooLarge @@ -711,7 +714,8 @@ class DynamoHandler(BaseResponse): attribute_updates = self.body.get("AttributeUpdates") expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) - existing_item = self.dynamodb_backend.get_item(name, key) + # We need to copy the item in order to avoid it being modified by the update_item operation + existing_item = copy.deepcopy(self.dynamodb_backend.get_item(name, key)) if existing_item: existing_attributes = existing_item.to_json()["Attributes"] else: @@ -797,14 +801,39 @@ class DynamoHandler(BaseResponse): k: v for k, v in existing_attributes.items() if k in changed_attributes } elif return_values == "UPDATED_NEW": - item_dict["Attributes"] = { - k: v - for k, v in item_dict["Attributes"].items() - if k in changed_attributes - } + item_dict["Attributes"] = self._build_updated_new_attributes( + existing_attributes, item_dict["Attributes"] + ) return dynamo_json_dump(item_dict) + def _build_updated_new_attributes(self, original, changed): + if type(changed) != type(original): + return changed + else: + if type(changed) is dict: + return { + key: self._build_updated_new_attributes( + original.get(key, None), changed[key] + ) + for key in changed.keys() + if changed[key] != original.get(key, None) + } + elif type(changed) in (set, list): + if len(changed) != len(original): + return changed + else: + return [ + self._build_updated_new_attributes( + original[index], changed[index] + ) + for index in range(len(changed)) + ] + elif changed != original: + return changed + else: + return None + def describe_limits(self): return json.dumps( { diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 82f82ccc9..63ee44d53 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -3431,13 +3431,18 @@ def test_update_supports_list_append(): ) # Update item using list_append expression - client.update_item( + updated_item = client.update_item( TableName="TestTable", Key={"SHA256": {"S": "sha-of-file"}}, UpdateExpression="SET crontab = list_append(crontab, :i)", ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, + ReturnValues="UPDATED_NEW", ) + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"crontab": {"L": [{"S": "bar1"}, {"S": "bar2"}]}} + ) # Verify item is appended to the existing list result = client.get_item( TableName="TestTable", Key={"SHA256": {"S": "sha-of-file"}} @@ -3470,15 +3475,19 @@ def test_update_supports_nested_list_append(): ) # Update item using list_append expression - client.update_item( + updated_item = client.update_item( TableName="TestTable", Key={"id": {"S": "nested_list_append"}}, UpdateExpression="SET a.#b = list_append(a.#b, :i)", ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeNames={"#b": "b"}, + ReturnValues="UPDATED_NEW", ) - # Verify item is appended to the existing list + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"a": {"M": {"b": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}}} + ) result = client.get_item( TableName="TestTable", Key={"id": {"S": "nested_list_append"}} )["Item"] @@ -3510,14 +3519,19 @@ def test_update_supports_multiple_levels_nested_list_append(): ) # Update item using list_append expression - client.update_item( + updated_item = client.update_item( TableName="TestTable", Key={"id": {"S": "nested_list_append"}}, UpdateExpression="SET a.#b.c = list_append(a.#b.#c, :i)", ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeNames={"#b": "b", "#c": "c"}, + ReturnValues="UPDATED_NEW", ) + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"a": {"M": {"b": {"M": {"c": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}}}}} + ) # Verify item is appended to the existing list result = client.get_item( TableName="TestTable", Key={"id": {"S": "nested_list_append"}} @@ -3551,14 +3565,19 @@ def test_update_supports_nested_list_append_onto_another_list(): ) # Update item using list_append expression - client.update_item( + updated_item = client.update_item( TableName="TestTable", Key={"id": {"S": "list_append_another"}}, UpdateExpression="SET a.#c = list_append(a.#b, :i)", ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeNames={"#b": "b", "#c": "c"}, + ReturnValues="UPDATED_NEW", ) + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"a": {"M": {"c": {"L": [{"S": "bar1"}, {"S": "bar2"}]}}}} + ) # Verify item is appended to the existing list result = client.get_item( TableName="TestTable", Key={"id": {"S": "list_append_another"}} @@ -3601,13 +3620,18 @@ def test_update_supports_list_append_maps(): ) # Update item using list_append expression - client.update_item( + updated_item = client.update_item( TableName="TestTable", Key={"id": {"S": "nested_list_append"}, "rid": {"S": "range_key"}}, UpdateExpression="SET a = list_append(a, :i)", ExpressionAttributeValues={":i": {"L": [{"M": {"b": {"S": "bar2"}}}]}}, + ReturnValues="UPDATED_NEW", ) + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"a": {"L": [{"M": {"b": {"S": "bar1"}}}, {"M": {"b": {"S": "bar2"}}}]}} + ) # Verify item is appended to the existing list result = client.query( TableName="TestTable", @@ -3643,11 +3667,18 @@ def test_update_supports_list_append_with_nested_if_not_exists_operation(): table = dynamo.Table(table_name) table.put_item(Item={"Id": "item-id", "nest1": {"nest2": {}}}) - table.update_item( + updated_item = table.update_item( Key={"Id": "item-id"}, UpdateExpression="SET nest1.nest2.event_history = list_append(if_not_exists(nest1.nest2.event_history, :empty_list), :new_value)", ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_value"]}, + ReturnValues="UPDATED_NEW", ) + + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"nest1": {"nest2": {"event_history": ["some_value"]}}} + ) + table.get_item(Key={"Id": "item-id"})["Item"].should.equal( {"Id": "item-id", "nest1": {"nest2": {"event_history": ["some_value"]}}} ) @@ -3668,11 +3699,18 @@ def test_update_supports_list_append_with_nested_if_not_exists_operation_and_pro table = dynamo.Table(table_name) table.put_item(Item={"Id": "item-id", "event_history": ["other_value"]}) - table.update_item( + updated_item = table.update_item( Key={"Id": "item-id"}, UpdateExpression="SET event_history = list_append(if_not_exists(event_history, :empty_list), :new_value)", ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_value"]}, + ReturnValues="UPDATED_NEW", ) + + # Verify updated item is correct + updated_item["Attributes"].should.equal( + {"event_history": ["other_value", "some_value"]} + ) + table.get_item(Key={"Id": "item-id"})["Item"].should.equal( {"Id": "item-id", "event_history": ["other_value", "some_value"]} ) @@ -3759,11 +3797,16 @@ def test_update_nested_item_if_original_value_is_none(): ) table = dynamo.Table("origin-rbu-dev") table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}}) - table.update_item( + updated_item = table.update_item( Key={"job_id": "a"}, UpdateExpression="SET job_details.job_name = :output", ExpressionAttributeValues={":output": "updated"}, + ReturnValues="UPDATED_NEW", ) + + # Verify updated item is correct + updated_item["Attributes"].should.equal({"job_details": {"job_name": "updated"}}) + table.scan()["Items"][0]["job_details"]["job_name"].should.equal("updated") @@ -3779,11 +3822,16 @@ def test_allow_update_to_item_with_different_type(): table = dynamo.Table("origin-rbu-dev") table.put_item(Item={"job_id": "a", "job_details": {"job_name": {"nested": "yes"}}}) table.put_item(Item={"job_id": "b", "job_details": {"job_name": {"nested": "yes"}}}) - table.update_item( + updated_item = table.update_item( Key={"job_id": "a"}, UpdateExpression="SET job_details.job_name = :output", ExpressionAttributeValues={":output": "updated"}, + ReturnValues="UPDATED_NEW", ) + + # Verify updated item is correct + updated_item["Attributes"].should.equal({"job_details": {"job_name": "updated"}}) + table.get_item(Key={"job_id": "a"})["Item"]["job_details"][ "job_name" ].should.be.equal("updated")