[Bugfix] UpdateExpression using ADD from zero (#2975)
When using the ADD syntax to sum up different components the path that is provided is allowed to be non-existent. In such a case DynamoDB will initialize it depending on the type of the value. If it is a number it will be initialized with 0. If it is a set it will be initialized with an empty set.
This commit is contained in:
parent
f1f7ddb69d
commit
9e7803dc36
@ -1,6 +1,10 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
from moto.dynamodb2.exceptions import IncorrectOperandType, IncorrectDataType
|
from moto.dynamodb2.exceptions import (
|
||||||
|
IncorrectOperandType,
|
||||||
|
IncorrectDataType,
|
||||||
|
ProvidedKeyDoesNotExist,
|
||||||
|
)
|
||||||
from moto.dynamodb2.models import DynamoType
|
from moto.dynamodb2.models import DynamoType
|
||||||
from moto.dynamodb2.models.dynamo_type import DDBTypeConversion, DDBType
|
from moto.dynamodb2.models.dynamo_type import DDBTypeConversion, DDBType
|
||||||
from moto.dynamodb2.parsing.ast_nodes import (
|
from moto.dynamodb2.parsing.ast_nodes import (
|
||||||
@ -193,7 +197,18 @@ class AddExecutor(NodeExecutor):
|
|||||||
value_to_add = self.get_action_value()
|
value_to_add = self.get_action_value()
|
||||||
if isinstance(value_to_add, DynamoType):
|
if isinstance(value_to_add, DynamoType):
|
||||||
if value_to_add.is_set():
|
if value_to_add.is_set():
|
||||||
|
try:
|
||||||
current_string_set = self.get_item_at_end_of_path(item)
|
current_string_set = self.get_item_at_end_of_path(item)
|
||||||
|
except ProvidedKeyDoesNotExist:
|
||||||
|
current_string_set = DynamoType({value_to_add.type: []})
|
||||||
|
SetExecutor.set(
|
||||||
|
item_part_to_modify_with_set=self.get_item_before_end_of_path(
|
||||||
|
item
|
||||||
|
),
|
||||||
|
element_to_set=self.get_element_to_action(),
|
||||||
|
value_to_set=current_string_set,
|
||||||
|
expression_attribute_names=self.expression_attribute_names,
|
||||||
|
)
|
||||||
assert isinstance(current_string_set, DynamoType)
|
assert isinstance(current_string_set, DynamoType)
|
||||||
if not current_string_set.type == value_to_add.type:
|
if not current_string_set.type == value_to_add.type:
|
||||||
raise IncorrectDataType()
|
raise IncorrectDataType()
|
||||||
@ -204,7 +219,11 @@ class AddExecutor(NodeExecutor):
|
|||||||
else:
|
else:
|
||||||
current_string_set.value.append(value)
|
current_string_set.value.append(value)
|
||||||
elif value_to_add.type == DDBType.NUMBER:
|
elif value_to_add.type == DDBType.NUMBER:
|
||||||
|
try:
|
||||||
existing_value = self.get_item_at_end_of_path(item)
|
existing_value = self.get_item_at_end_of_path(item)
|
||||||
|
except ProvidedKeyDoesNotExist:
|
||||||
|
existing_value = DynamoType({DDBType.NUMBER: "0"})
|
||||||
|
|
||||||
assert isinstance(existing_value, DynamoType)
|
assert isinstance(existing_value, DynamoType)
|
||||||
if not existing_value.type == DDBType.NUMBER:
|
if not existing_value.type == DDBType.NUMBER:
|
||||||
raise IncorrectDataType()
|
raise IncorrectDataType()
|
||||||
|
@ -5029,3 +5029,81 @@ def test_update_item_atomic_counter_return_values():
|
|||||||
"v" in response["Attributes"]
|
"v" in response["Attributes"]
|
||||||
), "v has been updated, and should be returned here"
|
), "v has been updated, and should be returned here"
|
||||||
response["Attributes"]["v"]["N"].should.equal("8")
|
response["Attributes"]["v"]["N"].should.equal("8")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_dynamodb2
|
||||||
|
def test_update_item_atomic_counter_from_zero():
|
||||||
|
table = "table_t"
|
||||||
|
ddb_mock = boto3.client("dynamodb", region_name="eu-west-1")
|
||||||
|
ddb_mock.create_table(
|
||||||
|
TableName=table,
|
||||||
|
KeySchema=[{"AttributeName": "t_id", "KeyType": "HASH"}],
|
||||||
|
AttributeDefinitions=[{"AttributeName": "t_id", "AttributeType": "S"}],
|
||||||
|
BillingMode="PAY_PER_REQUEST",
|
||||||
|
)
|
||||||
|
|
||||||
|
key = {"t_id": {"S": "item1"}}
|
||||||
|
|
||||||
|
ddb_mock.put_item(
|
||||||
|
TableName=table, Item=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
ddb_mock.update_item(
|
||||||
|
TableName=table,
|
||||||
|
Key=key,
|
||||||
|
UpdateExpression="add n_i :inc1, n_f :inc2",
|
||||||
|
ExpressionAttributeValues={":inc1": {"N": "1.2"}, ":inc2": {"N": "-0.5"}},
|
||||||
|
)
|
||||||
|
updated_item = ddb_mock.get_item(TableName=table, Key=key)["Item"]
|
||||||
|
assert updated_item["n_i"]["N"] == "1.2"
|
||||||
|
assert updated_item["n_f"]["N"] == "-0.5"
|
||||||
|
|
||||||
|
|
||||||
|
@mock_dynamodb2
|
||||||
|
def test_update_item_add_to_non_existent_set():
|
||||||
|
table = "table_t"
|
||||||
|
ddb_mock = boto3.client("dynamodb", region_name="eu-west-1")
|
||||||
|
ddb_mock.create_table(
|
||||||
|
TableName=table,
|
||||||
|
KeySchema=[{"AttributeName": "t_id", "KeyType": "HASH"}],
|
||||||
|
AttributeDefinitions=[{"AttributeName": "t_id", "AttributeType": "S"}],
|
||||||
|
BillingMode="PAY_PER_REQUEST",
|
||||||
|
)
|
||||||
|
key = {"t_id": {"S": "item1"}}
|
||||||
|
ddb_mock.put_item(
|
||||||
|
TableName=table, Item=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
ddb_mock.update_item(
|
||||||
|
TableName=table,
|
||||||
|
Key=key,
|
||||||
|
UpdateExpression="add s_i :s1",
|
||||||
|
ExpressionAttributeValues={":s1": {"SS": ["hello"]}},
|
||||||
|
)
|
||||||
|
updated_item = ddb_mock.get_item(TableName=table, Key=key)["Item"]
|
||||||
|
assert updated_item["s_i"]["SS"] == ["hello"]
|
||||||
|
|
||||||
|
|
||||||
|
@mock_dynamodb2
|
||||||
|
def test_update_item_add_to_non_existent_number_set():
|
||||||
|
table = "table_t"
|
||||||
|
ddb_mock = boto3.client("dynamodb", region_name="eu-west-1")
|
||||||
|
ddb_mock.create_table(
|
||||||
|
TableName=table,
|
||||||
|
KeySchema=[{"AttributeName": "t_id", "KeyType": "HASH"}],
|
||||||
|
AttributeDefinitions=[{"AttributeName": "t_id", "AttributeType": "S"}],
|
||||||
|
BillingMode="PAY_PER_REQUEST",
|
||||||
|
)
|
||||||
|
key = {"t_id": {"S": "item1"}}
|
||||||
|
ddb_mock.put_item(
|
||||||
|
TableName=table, Item=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
ddb_mock.update_item(
|
||||||
|
TableName=table,
|
||||||
|
Key=key,
|
||||||
|
UpdateExpression="add s_i :s1",
|
||||||
|
ExpressionAttributeValues={":s1": {"NS": ["3"]}},
|
||||||
|
)
|
||||||
|
updated_item = ddb_mock.get_item(TableName=table, Key=key)["Item"]
|
||||||
|
assert updated_item["s_i"]["NS"] == ["3"]
|
||||||
|
@ -1307,16 +1307,16 @@ def test_update_item_add_with_expression():
|
|||||||
ExpressionAttributeValues={":v": {"item4"}},
|
ExpressionAttributeValues={":v": {"item4"}},
|
||||||
)
|
)
|
||||||
current_item["str_set"] = current_item["str_set"].union({"item4"})
|
current_item["str_set"] = current_item["str_set"].union({"item4"})
|
||||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(current_item)
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Update item to add a string value to a non-existing set
|
# Update item to add a string value to a non-existing set
|
||||||
# Should throw: 'The provided key element does not match the schema'
|
table.update_item(
|
||||||
assert_failure_due_to_key_not_in_schema(
|
|
||||||
table.update_item,
|
|
||||||
Key=item_key,
|
Key=item_key,
|
||||||
UpdateExpression="ADD non_existing_str_set :v",
|
UpdateExpression="ADD non_existing_str_set :v",
|
||||||
ExpressionAttributeValues={":v": {"item4"}},
|
ExpressionAttributeValues={":v": {"item4"}},
|
||||||
)
|
)
|
||||||
|
current_item["non_existing_str_set"] = {"item4"}
|
||||||
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Update item to add a num value to a num set
|
# Update item to add a num value to a num set
|
||||||
table.update_item(
|
table.update_item(
|
||||||
@ -1325,7 +1325,7 @@ def test_update_item_add_with_expression():
|
|||||||
ExpressionAttributeValues={":v": {6}},
|
ExpressionAttributeValues={":v": {6}},
|
||||||
)
|
)
|
||||||
current_item["num_set"] = current_item["num_set"].union({6})
|
current_item["num_set"] = current_item["num_set"].union({6})
|
||||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(current_item)
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Update item to add a value to a number value
|
# Update item to add a value to a number value
|
||||||
table.update_item(
|
table.update_item(
|
||||||
@ -1334,7 +1334,7 @@ def test_update_item_add_with_expression():
|
|||||||
ExpressionAttributeValues={":v": 20},
|
ExpressionAttributeValues={":v": 20},
|
||||||
)
|
)
|
||||||
current_item["num_val"] = current_item["num_val"] + 20
|
current_item["num_val"] = current_item["num_val"] + 20
|
||||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(current_item)
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Attempt to add a number value to a string set, should raise Client Error
|
# Attempt to add a number value to a string set, should raise Client Error
|
||||||
table.update_item.when.called_with(
|
table.update_item.when.called_with(
|
||||||
@ -1342,7 +1342,7 @@ def test_update_item_add_with_expression():
|
|||||||
UpdateExpression="ADD str_set :v",
|
UpdateExpression="ADD str_set :v",
|
||||||
ExpressionAttributeValues={":v": 20},
|
ExpressionAttributeValues={":v": 20},
|
||||||
).should.have.raised(ClientError)
|
).should.have.raised(ClientError)
|
||||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(current_item)
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Attempt to add a number set to the string set, should raise a ClientError
|
# Attempt to add a number set to the string set, should raise a ClientError
|
||||||
table.update_item.when.called_with(
|
table.update_item.when.called_with(
|
||||||
@ -1350,7 +1350,7 @@ def test_update_item_add_with_expression():
|
|||||||
UpdateExpression="ADD str_set :v",
|
UpdateExpression="ADD str_set :v",
|
||||||
ExpressionAttributeValues={":v": {20}},
|
ExpressionAttributeValues={":v": {20}},
|
||||||
).should.have.raised(ClientError)
|
).should.have.raised(ClientError)
|
||||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(current_item)
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Attempt to update with a bad expression
|
# Attempt to update with a bad expression
|
||||||
table.update_item.when.called_with(
|
table.update_item.when.called_with(
|
||||||
@ -1388,17 +1388,18 @@ def test_update_item_add_with_nested_sets():
|
|||||||
current_item["nested"]["str_set"] = current_item["nested"]["str_set"].union(
|
current_item["nested"]["str_set"] = current_item["nested"]["str_set"].union(
|
||||||
{"item4"}
|
{"item4"}
|
||||||
)
|
)
|
||||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(current_item)
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
# Update item to add a string value to a non-existing set
|
# Update item to add a string value to a non-existing set
|
||||||
# Should raise
|
# Should raise
|
||||||
assert_failure_due_to_key_not_in_schema(
|
table.update_item(
|
||||||
table.update_item,
|
|
||||||
Key=item_key,
|
Key=item_key,
|
||||||
UpdateExpression="ADD #ns.#ne :v",
|
UpdateExpression="ADD #ns.#ne :v",
|
||||||
ExpressionAttributeNames={"#ns": "nested", "#ne": "non_existing_str_set"},
|
ExpressionAttributeNames={"#ns": "nested", "#ne": "non_existing_str_set"},
|
||||||
ExpressionAttributeValues={":v": {"new_item"}},
|
ExpressionAttributeValues={":v": {"new_item"}},
|
||||||
)
|
)
|
||||||
|
current_item["nested"]["non_existing_str_set"] = {"new_item"}
|
||||||
|
assert dict(table.get_item(Key=item_key)["Item"]) == current_item
|
||||||
|
|
||||||
|
|
||||||
@mock_dynamodb2
|
@mock_dynamodb2
|
||||||
|
Loading…
Reference in New Issue
Block a user