Add support for empty strings in non-key dynamo attributes (#3467)

* Add support for empty strings in non-key attributes

https://github.com/spulec/moto/issues/3339

* Nose, not pytest

* Revert "Nose, not pytest"

This reverts commit 5a3cf6c887dd9fafa49096c82cfa3a3b7f91d224.

* PUT is default action
This commit is contained in:
Rich Unger 2020-11-17 01:12:39 -08:00 committed by GitHub
parent 62d382ff70
commit f045af7e0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 257 additions and 52 deletions

View File

@ -164,3 +164,10 @@ class TransactionCanceledException(ValueError):
def __init__(self, errors): def __init__(self, errors):
msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors])) msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors]))
super(TransactionCanceledException, self).__init__(msg) super(TransactionCanceledException, self).__init__(msg)
class EmptyKeyAttributeException(MockValidationException):
empty_str_msg = "One or more parameter values were invalid: An AttributeValue may not contain an empty string"
def __init__(self):
super(EmptyKeyAttributeException, self).__init__(self.empty_str_msg)

View File

@ -20,6 +20,7 @@ from moto.dynamodb2.exceptions import (
ItemSizeToUpdateTooLarge, ItemSizeToUpdateTooLarge,
ConditionalCheckFailed, ConditionalCheckFailed,
TransactionCanceledException, TransactionCanceledException,
EmptyKeyAttributeException,
) )
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
@ -107,6 +108,13 @@ class Item(BaseModel):
included = self.attrs included = self.attrs
return {"Item": included} return {"Item": included}
def validate_no_empty_key_values(self, attribute_updates, key_attributes):
for attribute_name, update_action in attribute_updates.items():
action = update_action.get("Action") or "PUT" # PUT is default
new_value = next(iter(update_action["Value"].values()))
if action == "PUT" and new_value == "" and attribute_name in key_attributes:
raise EmptyKeyAttributeException
def update_with_attribute_updates(self, attribute_updates): def update_with_attribute_updates(self, attribute_updates):
for attribute_name, update_action in attribute_updates.items(): for attribute_name, update_action in attribute_updates.items():
# Use default Action value, if no explicit Action is passed. # Use default Action value, if no explicit Action is passed.
@ -434,6 +442,18 @@ class Table(CloudFormationModel):
def physical_resource_id(self): def physical_resource_id(self):
return self.name return self.name
@property
def key_attributes(self):
# A set of all the hash or range attributes for all indexes
def keys_from_index(idx):
schema = idx.schema
return [attr["AttributeName"] for attr in schema]
fieldnames = copy.copy(self.table_key_attrs)
for idx in self.indexes + self.global_indexes:
fieldnames += keys_from_index(idx)
return fieldnames
@staticmethod @staticmethod
def cloudformation_name_type(): def cloudformation_name_type():
return "TableName" return "TableName"
@ -1273,12 +1293,16 @@ class DynamoDBBackend(BaseBackend):
table.put_item(data) table.put_item(data)
item = table.get_item(hash_value, range_value) item = table.get_item(hash_value, range_value)
if attribute_updates:
item.validate_no_empty_key_values(attribute_updates, table.key_attributes)
if update_expression: if update_expression:
validated_ast = UpdateExpressionValidator( validated_ast = UpdateExpressionValidator(
update_expression_ast, update_expression_ast,
expression_attribute_names=expression_attribute_names, expression_attribute_names=expression_attribute_names,
expression_attribute_values=expression_attribute_values, expression_attribute_values=expression_attribute_values,
item=item, item=item,
table=table,
).validate() ).validate()
try: try:
UpdateExpressionExecutor( UpdateExpressionExecutor(

View File

@ -12,6 +12,7 @@ from moto.dynamodb2.exceptions import (
IncorrectOperandType, IncorrectOperandType,
InvalidUpdateExpressionInvalidDocumentPath, InvalidUpdateExpressionInvalidDocumentPath,
ProvidedKeyDoesNotExist, ProvidedKeyDoesNotExist,
EmptyKeyAttributeException,
) )
from moto.dynamodb2.models import DynamoType from moto.dynamodb2.models import DynamoType
from moto.dynamodb2.parsing.ast_nodes import ( from moto.dynamodb2.parsing.ast_nodes import (
@ -318,13 +319,36 @@ class ExecuteOperations(DepthFirstTraverser):
raise IncorrectOperandType("-", left_operand.type) raise IncorrectOperandType("-", left_operand.type)
class EmptyStringKeyValueValidator(DepthFirstTraverser):
def __init__(self, key_attributes):
self.key_attributes = key_attributes
def _processing_map(self):
return {UpdateExpressionSetAction: self.check_for_empty_string_key_value}
def check_for_empty_string_key_value(self, node):
"""A node representing a SET action. Check that keys are not being assigned empty strings"""
assert isinstance(node, UpdateExpressionSetAction)
assert len(node.children) == 2
key = node.children[0].children[0].children[0]
val_node = node.children[1].children[0]
if val_node.type in ["S", "B"] and key in self.key_attributes:
raise EmptyKeyAttributeException
return node
class Validator(object): class Validator(object):
""" """
A validator is used to validate expressions which are passed in as an AST. A validator is used to validate expressions which are passed in as an AST.
""" """
def __init__( def __init__(
self, expression, expression_attribute_names, expression_attribute_values, item self,
expression,
expression_attribute_names,
expression_attribute_values,
item,
table,
): ):
""" """
Besides validation the Validator should also replace referenced parts of an item which is cheapest upon Besides validation the Validator should also replace referenced parts of an item which is cheapest upon
@ -339,6 +363,7 @@ class Validator(object):
self.expression_attribute_names = expression_attribute_names self.expression_attribute_names = expression_attribute_names
self.expression_attribute_values = expression_attribute_values self.expression_attribute_values = expression_attribute_values
self.item = item self.item = item
self.table = table
self.processors = self.get_ast_processors() self.processors = self.get_ast_processors()
self.node_to_validate = deepcopy(expression) self.node_to_validate = deepcopy(expression)
@ -364,5 +389,6 @@ class UpdateExpressionValidator(Validator):
UpdateExpressionFunctionEvaluator(), UpdateExpressionFunctionEvaluator(),
NoneExistingPathChecker(), NoneExistingPathChecker(),
ExecuteOperations(), ExecuteOperations(),
EmptyStringKeyValueValidator(self.table.key_attributes),
] ]
return processors return processors

View File

@ -21,15 +21,18 @@ from moto.dynamodb2.models import dynamodb_backends, dynamo_json_dump
TRANSACTION_MAX_ITEMS = 25 TRANSACTION_MAX_ITEMS = 25
def has_empty_keys_or_values(_dict): def put_has_empty_keys(field_updates, table):
if _dict == "": if table:
return True key_names = table.key_attributes
if not isinstance(_dict, dict):
# string/binary fields with empty string as value
empty_str_fields = [
key
for (key, val) in field_updates.items()
if next(iter(val.keys())) in ["S", "B"] and next(iter(val.values())) == ""
]
return any([keyname in empty_str_fields for keyname in key_names])
return False return False
return any(
key == "" or value == "" or has_empty_keys_or_values(value)
for key, value in _dict.items()
)
def get_empty_str_error(): def get_empty_str_error():
@ -257,7 +260,7 @@ class DynamoHandler(BaseResponse):
er = "com.amazonaws.dynamodb.v20111205#ValidationException" er = "com.amazonaws.dynamodb.v20111205#ValidationException"
return self.error(er, "Return values set to invalid value") return self.error(er, "Return values set to invalid value")
if has_empty_keys_or_values(item): if put_has_empty_keys(item, self.dynamodb_backend.get_table(name)):
return get_empty_str_error() return get_empty_str_error()
overwrite = "Expected" not in self.body overwrite = "Expected" not in self.body
@ -751,9 +754,6 @@ class DynamoHandler(BaseResponse):
er = "com.amazonaws.dynamodb.v20111205#ValidationException" er = "com.amazonaws.dynamodb.v20111205#ValidationException"
return self.error(er, "Return values set to invalid value") return self.error(er, "Return values set to invalid value")
if has_empty_keys_or_values(expression_attribute_values):
return get_empty_str_error()
if "Expected" in self.body: if "Expected" in self.body:
expected = self.body["Expected"] expected = self.body["Expected"]
else: else:

View File

@ -0,0 +1,13 @@
import pytest
from moto.dynamodb2.models import Table
@pytest.fixture
def table():
return Table(
"Forums",
schema=[
{"KeyType": "HASH", "AttributeName": "forum_name"},
{"KeyType": "RANGE", "AttributeName": "subject"},
],
)

View File

@ -186,7 +186,7 @@ def test_list_not_found_table_tags():
@requires_boto_gte("2.9") @requires_boto_gte("2.9")
@mock_dynamodb2 @mock_dynamodb2
def test_item_add_empty_string_exception(): def test_item_add_empty_string_in_key_exception():
name = "TestTable" name = "TestTable"
conn = boto3.client( conn = boto3.client(
"dynamodb", "dynamodb",
@ -205,10 +205,10 @@ def test_item_add_empty_string_exception():
conn.put_item( conn.put_item(
TableName=name, TableName=name,
Item={ Item={
"forum_name": {"S": "LOLCat Forum"}, "forum_name": {"S": ""},
"subject": {"S": "Check this out!"}, "subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"}, "Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": ""}, "SentBy": {"S": "someone@somewhere.edu"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"}, "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
}, },
) )
@ -222,7 +222,36 @@ def test_item_add_empty_string_exception():
@requires_boto_gte("2.9") @requires_boto_gte("2.9")
@mock_dynamodb2 @mock_dynamodb2
def test_update_item_with_empty_string_exception(): def test_item_add_empty_string_no_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": ""},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_update_item_with_empty_string_in_key_exception():
name = "TestTable" name = "TestTable"
conn = boto3.client( conn = boto3.client(
"dynamodb", "dynamodb",
@ -252,8 +281,8 @@ def test_update_item_with_empty_string_exception():
conn.update_item( conn.update_item(
TableName=name, TableName=name,
Key={"forum_name": {"S": "LOLCat Forum"}}, Key={"forum_name": {"S": "LOLCat Forum"}},
UpdateExpression="set Body=:Body", UpdateExpression="set forum_name=:NewName",
ExpressionAttributeValues={":Body": {"S": ""}}, ExpressionAttributeValues={":NewName": {"S": ""}},
) )
ex.value.response["Error"]["Code"].should.equal("ValidationException") ex.value.response["Error"]["Code"].should.equal("ValidationException")
@ -263,6 +292,42 @@ def test_update_item_with_empty_string_exception():
) )
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_update_item_with_empty_string_no_exception():
name = "TestTable"
conn = boto3.client(
"dynamodb",
region_name="us-west-2",
aws_access_key_id="ak",
aws_secret_access_key="sk",
)
conn.create_table(
TableName=name,
KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
)
conn.put_item(
TableName=name,
Item={
"forum_name": {"S": "LOLCat Forum"},
"subject": {"S": "Check this out!"},
"Body": {"S": "http://url_to_lolcat.gif"},
"SentBy": {"S": "test"},
"ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
},
)
conn.update_item(
TableName=name,
Key={"forum_name": {"S": "LOLCat Forum"}},
UpdateExpression="set Body=:Body",
ExpressionAttributeValues={":Body": {"S": ""}},
)
@requires_boto_gte("2.9") @requires_boto_gte("2.9")
@mock_dynamodb2 @mock_dynamodb2
def test_query_invalid_table(): def test_query_invalid_table():

View File

@ -7,7 +7,7 @@ from moto.dynamodb2.parsing.expressions import UpdateExpressionParser
from moto.dynamodb2.parsing.validators import UpdateExpressionValidator from moto.dynamodb2.parsing.validators import UpdateExpressionValidator
def test_execution_of_if_not_exists_not_existing_value(): def test_execution_of_if_not_exists_not_existing_value(table):
update_expression = "SET a = if_not_exists(b, a)" update_expression = "SET a = if_not_exists(b, a)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -22,6 +22,7 @@ def test_execution_of_if_not_exists_not_existing_value():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -34,7 +35,9 @@ def test_execution_of_if_not_exists_not_existing_value():
assert expected_item == item assert expected_item == item
def test_execution_of_if_not_exists_with_existing_attribute_should_return_attribute(): def test_execution_of_if_not_exists_with_existing_attribute_should_return_attribute(
table,
):
update_expression = "SET a = if_not_exists(b, a)" update_expression = "SET a = if_not_exists(b, a)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -49,6 +52,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_attrib
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -61,7 +65,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_attrib
assert expected_item == item assert expected_item == item
def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(): def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(table):
update_expression = "SET a = if_not_exists(b, :val)" update_expression = "SET a = if_not_exists(b, :val)"
update_expression_values = {":val": {"N": "4"}} update_expression_values = {":val": {"N": "4"}}
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -77,6 +81,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=update_expression_values, expression_attribute_values=update_expression_values,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -89,7 +94,9 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(
assert expected_item == item assert expected_item == item
def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_value(): def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_value(
table,
):
update_expression = "SET a = if_not_exists(b, :val)" update_expression = "SET a = if_not_exists(b, :val)"
update_expression_values = {":val": {"N": "4"}} update_expression_values = {":val": {"N": "4"}}
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -105,6 +112,7 @@ def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_va
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=update_expression_values, expression_attribute_values=update_expression_values,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -117,7 +125,7 @@ def test_execution_of_if_not_exists_with_non_existing_attribute_should_return_va
assert expected_item == item assert expected_item == item
def test_execution_of_sum_operation(): def test_execution_of_sum_operation(table):
update_expression = "SET a = a + b" update_expression = "SET a = a + b"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -132,6 +140,7 @@ def test_execution_of_sum_operation():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -144,7 +153,7 @@ def test_execution_of_sum_operation():
assert expected_item == item assert expected_item == item
def test_execution_of_remove(): def test_execution_of_remove(table):
update_expression = "Remove a" update_expression = "Remove a"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -159,6 +168,7 @@ def test_execution_of_remove():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -171,7 +181,7 @@ def test_execution_of_remove():
assert expected_item == item assert expected_item == item
def test_execution_of_remove_in_map(): def test_execution_of_remove_in_map(table):
update_expression = "Remove itemmap.itemlist[1].foo11" update_expression = "Remove itemmap.itemlist[1].foo11"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -198,6 +208,7 @@ def test_execution_of_remove_in_map():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -222,7 +233,7 @@ def test_execution_of_remove_in_map():
assert expected_item == item assert expected_item == item
def test_execution_of_remove_in_list(): def test_execution_of_remove_in_list(table):
update_expression = "Remove itemmap.itemlist[1]" update_expression = "Remove itemmap.itemlist[1]"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -249,6 +260,7 @@ def test_execution_of_remove_in_list():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -270,7 +282,7 @@ def test_execution_of_remove_in_list():
assert expected_item == item assert expected_item == item
def test_execution_of_delete_element_from_set(): def test_execution_of_delete_element_from_set(table):
update_expression = "delete s :value" update_expression = "delete s :value"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -285,6 +297,7 @@ def test_execution_of_delete_element_from_set():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":value": {"SS": ["value2", "value5"]}}, expression_attribute_values={":value": {"SS": ["value2", "value5"]}},
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -297,7 +310,7 @@ def test_execution_of_delete_element_from_set():
assert expected_item == item assert expected_item == item
def test_execution_of_add_number(): def test_execution_of_add_number(table):
update_expression = "add s :value" update_expression = "add s :value"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -312,6 +325,7 @@ def test_execution_of_add_number():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":value": {"N": "10"}}, expression_attribute_values={":value": {"N": "10"}},
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -324,7 +338,7 @@ def test_execution_of_add_number():
assert expected_item == item assert expected_item == item
def test_execution_of_add_set_to_a_number(): def test_execution_of_add_set_to_a_number(table):
update_expression = "add s :value" update_expression = "add s :value"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -340,6 +354,7 @@ def test_execution_of_add_set_to_a_number():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":value": {"SS": ["s1"]}}, expression_attribute_values={":value": {"SS": ["s1"]}},
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -355,7 +370,7 @@ def test_execution_of_add_set_to_a_number():
assert True assert True
def test_execution_of_add_to_a_set(): def test_execution_of_add_to_a_set(table):
update_expression = "ADD s :value" update_expression = "ADD s :value"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -370,6 +385,7 @@ def test_execution_of_add_to_a_set():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":value": {"SS": ["value2", "value5"]}}, expression_attribute_values={":value": {"SS": ["value2", "value5"]}},
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
expected_item = Item( expected_item = Item(
@ -398,7 +414,7 @@ def test_execution_of_add_to_a_set():
], ],
) )
def test_execution_of__delete_element_from_set_invalid_value( def test_execution_of__delete_element_from_set_invalid_value(
expression_attribute_values, unexpected_data_type expression_attribute_values, unexpected_data_type, table
): ):
"""A delete statement must use a value of type SS in order to delete elements from a set.""" """A delete statement must use a value of type SS in order to delete elements from a set."""
update_expression = "delete s :value" update_expression = "delete s :value"
@ -416,6 +432,7 @@ def test_execution_of__delete_element_from_set_invalid_value(
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=expression_attribute_values, expression_attribute_values=expression_attribute_values,
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
assert False, "Must raise exception" assert False, "Must raise exception"
@ -424,7 +441,7 @@ def test_execution_of__delete_element_from_set_invalid_value(
assert e.operand_type == unexpected_data_type assert e.operand_type == unexpected_data_type
def test_execution_of_delete_element_from_a_string_attribute(): def test_execution_of_delete_element_from_a_string_attribute(table):
"""A delete statement must use a value of type SS in order to delete elements from a set.""" """A delete statement must use a value of type SS in order to delete elements from a set."""
update_expression = "delete s :value" update_expression = "delete s :value"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -441,6 +458,7 @@ def test_execution_of_delete_element_from_a_string_attribute():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":value": {"SS": ["value2"]}}, expression_attribute_values={":value": {"SS": ["value2"]}},
item=item, item=item,
table=table,
).validate() ).validate()
UpdateExpressionExecutor(validated_ast, item, None).execute() UpdateExpressionExecutor(validated_ast, item, None).execute()
assert False, "Must raise exception" assert False, "Must raise exception"

View File

@ -7,6 +7,7 @@ from moto.dynamodb2.exceptions import (
ExpressionAttributeNameNotDefined, ExpressionAttributeNameNotDefined,
IncorrectOperandType, IncorrectOperandType,
InvalidUpdateExpressionInvalidDocumentPath, InvalidUpdateExpressionInvalidDocumentPath,
EmptyKeyAttributeException,
) )
from moto.dynamodb2.models import Item, DynamoType from moto.dynamodb2.models import Item, DynamoType
from moto.dynamodb2.parsing.ast_nodes import ( from moto.dynamodb2.parsing.ast_nodes import (
@ -18,7 +19,28 @@ from moto.dynamodb2.parsing.expressions import UpdateExpressionParser
from moto.dynamodb2.parsing.validators import UpdateExpressionValidator from moto.dynamodb2.parsing.validators import UpdateExpressionValidator
def test_validation_of_update_expression_with_keyword(): def test_validation_of_empty_string_key_val(table):
with pytest.raises(EmptyKeyAttributeException):
update_expression = "set forum_name=:NewName"
update_expression_values = {":NewName": {"S": ""}}
update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item(
hash_key=DynamoType({"S": "forum_name"}),
hash_key_type="TYPE",
range_key=None,
range_key_type=None,
attrs={"forum_name": {"S": "hello"}},
)
UpdateExpressionValidator(
update_expression_ast,
expression_attribute_names=None,
expression_attribute_values=update_expression_values,
item=item,
table=table,
).validate()
def test_validation_of_update_expression_with_keyword(table):
try: try:
update_expression = "SET myNum = path + :val" update_expression = "SET myNum = path + :val"
update_expression_values = {":val": {"N": "3"}} update_expression_values = {":val": {"N": "3"}}
@ -35,6 +57,7 @@ def test_validation_of_update_expression_with_keyword():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=update_expression_values, expression_attribute_values=update_expression_values,
item=item, item=item,
table=table,
).validate() ).validate()
assert False, "No exception raised" assert False, "No exception raised"
except AttributeIsReservedKeyword as e: except AttributeIsReservedKeyword as e:
@ -44,7 +67,9 @@ def test_validation_of_update_expression_with_keyword():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"update_expression", ["SET a = #b + :val2", "SET a = :val2 + #b",] "update_expression", ["SET a = #b + :val2", "SET a = :val2 + #b",]
) )
def test_validation_of_a_set_statement_with_incorrect_passed_value(update_expression): def test_validation_of_a_set_statement_with_incorrect_passed_value(
update_expression, table
):
""" """
By running permutations it shows that values are replaced prior to resolving attributes. By running permutations it shows that values are replaced prior to resolving attributes.
@ -65,12 +90,15 @@ def test_validation_of_a_set_statement_with_incorrect_passed_value(update_expres
expression_attribute_names={"#b": "ok"}, expression_attribute_names={"#b": "ok"},
expression_attribute_values={":val": {"N": "3"}}, expression_attribute_values={":val": {"N": "3"}},
item=item, item=item,
table=table,
).validate() ).validate()
except ExpressionAttributeValueNotDefined as e: except ExpressionAttributeValueNotDefined as e:
assert e.attribute_value == ":val2" assert e.attribute_value == ":val2"
def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_item(): def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_item(
table,
):
""" """
When an update expression tries to get an attribute that does not exist it must throw the appropriate exception. When an update expression tries to get an attribute that does not exist it must throw the appropriate exception.
@ -92,6 +120,7 @@ def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_i
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
assert False, "No exception raised" assert False, "No exception raised"
except AttributeDoesNotExist: except AttributeDoesNotExist:
@ -100,7 +129,7 @@ def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_i
@pytest.mark.parametrize("update_expression", ["SET a = #c", "SET a = #c + #d",]) @pytest.mark.parametrize("update_expression", ["SET a = #c", "SET a = #c + #d",])
def test_validation_of_update_expression_with_attribute_name_that_is_not_defined( def test_validation_of_update_expression_with_attribute_name_that_is_not_defined(
update_expression, update_expression, table,
): ):
""" """
When an update expression tries to get an attribute name that is not provided it must throw an exception. When an update expression tries to get an attribute name that is not provided it must throw an exception.
@ -122,13 +151,14 @@ def test_validation_of_update_expression_with_attribute_name_that_is_not_defined
expression_attribute_names={"#b": "ok"}, expression_attribute_names={"#b": "ok"},
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
assert False, "No exception raised" assert False, "No exception raised"
except ExpressionAttributeNameNotDefined as e: except ExpressionAttributeNameNotDefined as e:
assert e.not_defined_attribute_name == "#c" assert e.not_defined_attribute_name == "#c"
def test_validation_of_if_not_exists_not_existing_invalid_replace_value(): def test_validation_of_if_not_exists_not_existing_invalid_replace_value(table):
try: try:
update_expression = "SET a = if_not_exists(b, a.c)" update_expression = "SET a = if_not_exists(b, a.c)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -144,6 +174,7 @@ def test_validation_of_if_not_exists_not_existing_invalid_replace_value():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
assert False, "No exception raised" assert False, "No exception raised"
except AttributeDoesNotExist: except AttributeDoesNotExist:
@ -172,7 +203,7 @@ def get_set_action_value(ast):
return dynamo_value return dynamo_value
def test_validation_of_if_not_exists_not_existing_value(): def test_validation_of_if_not_exists_not_existing_value(table):
update_expression = "SET a = if_not_exists(b, a)" update_expression = "SET a = if_not_exists(b, a)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -187,12 +218,15 @@ def test_validation_of_if_not_exists_not_existing_value():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"S": "A"}) assert dynamo_value == DynamoType({"S": "A"})
def test_validation_of_if_not_exists_with_existing_attribute_should_return_attribute(): def test_validation_of_if_not_exists_with_existing_attribute_should_return_attribute(
table,
):
update_expression = "SET a = if_not_exists(b, a)" update_expression = "SET a = if_not_exists(b, a)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -207,12 +241,13 @@ def test_validation_of_if_not_exists_with_existing_attribute_should_return_attri
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"S": "B"}) assert dynamo_value == DynamoType({"S": "B"})
def test_validation_of_if_not_exists_with_existing_attribute_should_return_value(): def test_validation_of_if_not_exists_with_existing_attribute_should_return_value(table):
update_expression = "SET a = if_not_exists(b, :val)" update_expression = "SET a = if_not_exists(b, :val)"
update_expression_values = {":val": {"N": "4"}} update_expression_values = {":val": {"N": "4"}}
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -228,12 +263,15 @@ def test_validation_of_if_not_exists_with_existing_attribute_should_return_value
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=update_expression_values, expression_attribute_values=update_expression_values,
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"N": "3"}) assert dynamo_value == DynamoType({"N": "3"})
def test_validation_of_if_not_exists_with_non_existing_attribute_should_return_value(): def test_validation_of_if_not_exists_with_non_existing_attribute_should_return_value(
table,
):
update_expression = "SET a = if_not_exists(b, :val)" update_expression = "SET a = if_not_exists(b, :val)"
update_expression_values = {":val": {"N": "4"}} update_expression_values = {":val": {"N": "4"}}
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -249,12 +287,13 @@ def test_validation_of_if_not_exists_with_non_existing_attribute_should_return_v
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=update_expression_values, expression_attribute_values=update_expression_values,
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"N": "4"}) assert dynamo_value == DynamoType({"N": "4"})
def test_validation_of_sum_operation(): def test_validation_of_sum_operation(table):
update_expression = "SET a = a + b" update_expression = "SET a = a + b"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -269,12 +308,13 @@ def test_validation_of_sum_operation():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"N": "7"}) assert dynamo_value == DynamoType({"N": "7"})
def test_validation_homogeneous_list_append_function(): def test_validation_homogeneous_list_append_function(table):
update_expression = "SET ri = list_append(ri, :vals)" update_expression = "SET ri = list_append(ri, :vals)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -289,6 +329,7 @@ def test_validation_homogeneous_list_append_function():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":vals": {"L": [{"S": "i3"}, {"S": "i4"}]}}, expression_attribute_values={":vals": {"L": [{"S": "i3"}, {"S": "i4"}]}},
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType( assert dynamo_value == DynamoType(
@ -296,7 +337,7 @@ def test_validation_homogeneous_list_append_function():
) )
def test_validation_hetereogenous_list_append_function(): def test_validation_hetereogenous_list_append_function(table):
update_expression = "SET ri = list_append(ri, :vals)" update_expression = "SET ri = list_append(ri, :vals)"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -311,12 +352,13 @@ def test_validation_hetereogenous_list_append_function():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":vals": {"L": [{"N": "3"}]}}, expression_attribute_values={":vals": {"L": [{"N": "3"}]}},
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"L": [{"S": "i1"}, {"S": "i2"}, {"N": "3"}]}) assert dynamo_value == DynamoType({"L": [{"S": "i1"}, {"S": "i2"}, {"N": "3"}]})
def test_validation_list_append_function_with_non_list_arg(): def test_validation_list_append_function_with_non_list_arg(table):
""" """
Must error out: Must error out:
Invalid UpdateExpression: Incorrect operand type for operator or function; Invalid UpdateExpression: Incorrect operand type for operator or function;
@ -339,13 +381,14 @@ def test_validation_list_append_function_with_non_list_arg():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":vals": {"S": "N"}}, expression_attribute_values={":vals": {"S": "N"}},
item=item, item=item,
table=table,
).validate() ).validate()
except IncorrectOperandType as e: except IncorrectOperandType as e:
assert e.operand_type == "S" assert e.operand_type == "S"
assert e.operator_or_function == "list_append" assert e.operator_or_function == "list_append"
def test_sum_with_incompatible_types(): def test_sum_with_incompatible_types(table):
""" """
Must error out: Must error out:
Invalid UpdateExpression: Incorrect operand type for operator or function; operator or function: +, operand type: S' Invalid UpdateExpression: Incorrect operand type for operator or function; operator or function: +, operand type: S'
@ -367,13 +410,14 @@ def test_sum_with_incompatible_types():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":val": {"S": "N"}, ":val2": {"N": "3"}}, expression_attribute_values={":val": {"S": "N"}, ":val2": {"N": "3"}},
item=item, item=item,
table=table,
).validate() ).validate()
except IncorrectOperandType as e: except IncorrectOperandType as e:
assert e.operand_type == "S" assert e.operand_type == "S"
assert e.operator_or_function == "+" assert e.operator_or_function == "+"
def test_validation_of_subraction_operation(): def test_validation_of_subraction_operation(table):
update_expression = "SET ri = :val - :val2" update_expression = "SET ri = :val - :val2"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
item = Item( item = Item(
@ -388,12 +432,13 @@ def test_validation_of_subraction_operation():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":val": {"N": "1"}, ":val2": {"N": "3"}}, expression_attribute_values={":val": {"N": "1"}, ":val2": {"N": "3"}},
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"N": "-2"}) assert dynamo_value == DynamoType({"N": "-2"})
def test_cannot_index_into_a_string(): def test_cannot_index_into_a_string(table):
""" """
Must error out: Must error out:
The document path provided in the update expression is invalid for update' The document path provided in the update expression is invalid for update'
@ -413,13 +458,16 @@ def test_cannot_index_into_a_string():
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values={":Item": {"S": "string_update"}}, expression_attribute_values={":Item": {"S": "string_update"}},
item=item, item=item,
table=table,
).validate() ).validate()
assert False, "Must raise exception" assert False, "Must raise exception"
except InvalidUpdateExpressionInvalidDocumentPath: except InvalidUpdateExpressionInvalidDocumentPath:
assert True assert True
def test_validation_set_path_does_not_need_to_be_resolvable_when_setting_a_new_attribute(): def test_validation_set_path_does_not_need_to_be_resolvable_when_setting_a_new_attribute(
table,
):
"""If this step just passes we are happy enough""" """If this step just passes we are happy enough"""
update_expression = "set d=a" update_expression = "set d=a"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -435,12 +483,15 @@ def test_validation_set_path_does_not_need_to_be_resolvable_when_setting_a_new_a
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
dynamo_value = get_set_action_value(validated_ast) dynamo_value = get_set_action_value(validated_ast)
assert dynamo_value == DynamoType({"N": "3"}) assert dynamo_value == DynamoType({"N": "3"})
def test_validation_set_path_does_not_need_to_be_resolvable_but_must_be_creatable_when_setting_a_new_attribute(): def test_validation_set_path_does_not_need_to_be_resolvable_but_must_be_creatable_when_setting_a_new_attribute(
table,
):
try: try:
update_expression = "set d.e=a" update_expression = "set d.e=a"
update_expression_ast = UpdateExpressionParser.make(update_expression) update_expression_ast = UpdateExpressionParser.make(update_expression)
@ -456,6 +507,7 @@ def test_validation_set_path_does_not_need_to_be_resolvable_but_must_be_creatabl
expression_attribute_names=None, expression_attribute_names=None,
expression_attribute_values=None, expression_attribute_values=None,
item=item, item=item,
table=table,
).validate() ).validate()
assert False, "Must raise exception" assert False, "Must raise exception"
except InvalidUpdateExpressionInvalidDocumentPath: except InvalidUpdateExpressionInvalidDocumentPath: