Merge pull request #2799 from ImFlog/fix_dynamodb_updated_new

Fix UPDATED_NEW return values differences
This commit is contained in:
Bert Blommers 2020-03-18 08:21:01 +00:00 committed by GitHub
commit 71bf314a0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 18 deletions

View File

@ -146,6 +146,9 @@ class DynamoType(object):
def __eq__(self, other): def __eq__(self, other):
return self.type == other.type and self.value == other.value 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): def __lt__(self, other):
return self.cast_value < other.cast_value return self.cast_value < other.cast_value

View File

@ -1,9 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools
import copy
import json import json
import six
import re import re
import itertools
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, InvalidUpdateExpression, ItemSizeTooLarge from .exceptions import InvalidIndexNameError, InvalidUpdateExpression, ItemSizeTooLarge
@ -711,7 +714,8 @@ class DynamoHandler(BaseResponse):
attribute_updates = self.body.get("AttributeUpdates") attribute_updates = self.body.get("AttributeUpdates")
expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) expression_attribute_names = self.body.get("ExpressionAttributeNames", {})
expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) 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: if existing_item:
existing_attributes = existing_item.to_json()["Attributes"] existing_attributes = existing_item.to_json()["Attributes"]
else: else:
@ -797,14 +801,39 @@ class DynamoHandler(BaseResponse):
k: v for k, v in existing_attributes.items() if k in changed_attributes k: v for k, v in existing_attributes.items() if k in changed_attributes
} }
elif return_values == "UPDATED_NEW": elif return_values == "UPDATED_NEW":
item_dict["Attributes"] = { item_dict["Attributes"] = self._build_updated_new_attributes(
k: v existing_attributes, item_dict["Attributes"]
for k, v in item_dict["Attributes"].items() )
if k in changed_attributes
}
return dynamo_json_dump(item_dict) 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): def describe_limits(self):
return json.dumps( return json.dumps(
{ {

View File

@ -3431,13 +3431,18 @@ def test_update_supports_list_append():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"SHA256": {"S": "sha-of-file"}}, Key={"SHA256": {"S": "sha-of-file"}},
UpdateExpression="SET crontab = list_append(crontab, :i)", UpdateExpression="SET crontab = list_append(crontab, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, 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 # Verify item is appended to the existing list
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"SHA256": {"S": "sha-of-file"}} 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 # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "nested_list_append"}}, Key={"id": {"S": "nested_list_append"}},
UpdateExpression="SET a.#b = list_append(a.#b, :i)", UpdateExpression="SET a.#b = list_append(a.#b, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ExpressionAttributeNames={"#b": "b"}, 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( result = client.get_item(
TableName="TestTable", Key={"id": {"S": "nested_list_append"}} TableName="TestTable", Key={"id": {"S": "nested_list_append"}}
)["Item"] )["Item"]
@ -3510,14 +3519,19 @@ def test_update_supports_multiple_levels_nested_list_append():
) )
# Update item using list_append expression # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "nested_list_append"}}, Key={"id": {"S": "nested_list_append"}},
UpdateExpression="SET a.#b.c = list_append(a.#b.#c, :i)", UpdateExpression="SET a.#b.c = list_append(a.#b.#c, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ExpressionAttributeNames={"#b": "b", "#c": "c"}, 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 # Verify item is appended to the existing list
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"id": {"S": "nested_list_append"}} 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 # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "list_append_another"}}, Key={"id": {"S": "list_append_another"}},
UpdateExpression="SET a.#c = list_append(a.#b, :i)", UpdateExpression="SET a.#c = list_append(a.#b, :i)",
ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}},
ExpressionAttributeNames={"#b": "b", "#c": "c"}, 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 # Verify item is appended to the existing list
result = client.get_item( result = client.get_item(
TableName="TestTable", Key={"id": {"S": "list_append_another"}} 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 # Update item using list_append expression
client.update_item( updated_item = client.update_item(
TableName="TestTable", TableName="TestTable",
Key={"id": {"S": "nested_list_append"}, "rid": {"S": "range_key"}}, Key={"id": {"S": "nested_list_append"}, "rid": {"S": "range_key"}},
UpdateExpression="SET a = list_append(a, :i)", UpdateExpression="SET a = list_append(a, :i)",
ExpressionAttributeValues={":i": {"L": [{"M": {"b": {"S": "bar2"}}}]}}, 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 # Verify item is appended to the existing list
result = client.query( result = client.query(
TableName="TestTable", TableName="TestTable",
@ -3643,11 +3667,18 @@ def test_update_supports_list_append_with_nested_if_not_exists_operation():
table = dynamo.Table(table_name) table = dynamo.Table(table_name)
table.put_item(Item={"Id": "item-id", "nest1": {"nest2": {}}}) table.put_item(Item={"Id": "item-id", "nest1": {"nest2": {}}})
table.update_item( updated_item = table.update_item(
Key={"Id": "item-id"}, Key={"Id": "item-id"},
UpdateExpression="SET nest1.nest2.event_history = list_append(if_not_exists(nest1.nest2.event_history, :empty_list), :new_value)", 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"]}, 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( table.get_item(Key={"Id": "item-id"})["Item"].should.equal(
{"Id": "item-id", "nest1": {"nest2": {"event_history": ["some_value"]}}} {"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 = dynamo.Table(table_name)
table.put_item(Item={"Id": "item-id", "event_history": ["other_value"]}) table.put_item(Item={"Id": "item-id", "event_history": ["other_value"]})
table.update_item( updated_item = table.update_item(
Key={"Id": "item-id"}, Key={"Id": "item-id"},
UpdateExpression="SET event_history = list_append(if_not_exists(event_history, :empty_list), :new_value)", UpdateExpression="SET event_history = list_append(if_not_exists(event_history, :empty_list), :new_value)",
ExpressionAttributeValues={":empty_list": [], ":new_value": ["some_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( table.get_item(Key={"Id": "item-id"})["Item"].should.equal(
{"Id": "item-id", "event_history": ["other_value", "some_value"]} {"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 = dynamo.Table("origin-rbu-dev")
table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}}) table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}})
table.update_item( updated_item = table.update_item(
Key={"job_id": "a"}, Key={"job_id": "a"},
UpdateExpression="SET job_details.job_name = :output", UpdateExpression="SET job_details.job_name = :output",
ExpressionAttributeValues={":output": "updated"}, 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") 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 = dynamo.Table("origin-rbu-dev")
table.put_item(Item={"job_id": "a", "job_details": {"job_name": {"nested": "yes"}}}) 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.put_item(Item={"job_id": "b", "job_details": {"job_name": {"nested": "yes"}}})
table.update_item( updated_item = table.update_item(
Key={"job_id": "a"}, Key={"job_id": "a"},
UpdateExpression="SET job_details.job_name = :output", UpdateExpression="SET job_details.job_name = :output",
ExpressionAttributeValues={":output": "updated"}, 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"][ table.get_item(Key={"job_id": "a"})["Item"]["job_details"][
"job_name" "job_name"
].should.be.equal("updated") ].should.be.equal("updated")