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:
parent
62d382ff70
commit
f045af7e0a
@ -164,3 +164,10 @@ class TransactionCanceledException(ValueError):
|
||||
def __init__(self, errors):
|
||||
msg = self.cancel_reason_msg.format(", ".join([str(err) for err in errors]))
|
||||
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)
|
||||
|
@ -20,6 +20,7 @@ from moto.dynamodb2.exceptions import (
|
||||
ItemSizeToUpdateTooLarge,
|
||||
ConditionalCheckFailed,
|
||||
TransactionCanceledException,
|
||||
EmptyKeyAttributeException,
|
||||
)
|
||||
from moto.dynamodb2.models.utilities import bytesize
|
||||
from moto.dynamodb2.models.dynamo_type import DynamoType
|
||||
@ -107,6 +108,13 @@ class Item(BaseModel):
|
||||
included = self.attrs
|
||||
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):
|
||||
for attribute_name, update_action in attribute_updates.items():
|
||||
# Use default Action value, if no explicit Action is passed.
|
||||
@ -434,6 +442,18 @@ class Table(CloudFormationModel):
|
||||
def physical_resource_id(self):
|
||||
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
|
||||
def cloudformation_name_type():
|
||||
return "TableName"
|
||||
@ -1273,12 +1293,16 @@ class DynamoDBBackend(BaseBackend):
|
||||
table.put_item(data)
|
||||
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:
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=expression_attribute_names,
|
||||
expression_attribute_values=expression_attribute_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
try:
|
||||
UpdateExpressionExecutor(
|
||||
|
@ -12,6 +12,7 @@ from moto.dynamodb2.exceptions import (
|
||||
IncorrectOperandType,
|
||||
InvalidUpdateExpressionInvalidDocumentPath,
|
||||
ProvidedKeyDoesNotExist,
|
||||
EmptyKeyAttributeException,
|
||||
)
|
||||
from moto.dynamodb2.models import DynamoType
|
||||
from moto.dynamodb2.parsing.ast_nodes import (
|
||||
@ -318,13 +319,36 @@ class ExecuteOperations(DepthFirstTraverser):
|
||||
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):
|
||||
"""
|
||||
A validator is used to validate expressions which are passed in as an AST.
|
||||
"""
|
||||
|
||||
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
|
||||
@ -339,6 +363,7 @@ class Validator(object):
|
||||
self.expression_attribute_names = expression_attribute_names
|
||||
self.expression_attribute_values = expression_attribute_values
|
||||
self.item = item
|
||||
self.table = table
|
||||
self.processors = self.get_ast_processors()
|
||||
self.node_to_validate = deepcopy(expression)
|
||||
|
||||
@ -364,5 +389,6 @@ class UpdateExpressionValidator(Validator):
|
||||
UpdateExpressionFunctionEvaluator(),
|
||||
NoneExistingPathChecker(),
|
||||
ExecuteOperations(),
|
||||
EmptyStringKeyValueValidator(self.table.key_attributes),
|
||||
]
|
||||
return processors
|
||||
|
@ -21,15 +21,18 @@ from moto.dynamodb2.models import dynamodb_backends, dynamo_json_dump
|
||||
TRANSACTION_MAX_ITEMS = 25
|
||||
|
||||
|
||||
def has_empty_keys_or_values(_dict):
|
||||
if _dict == "":
|
||||
return True
|
||||
if not isinstance(_dict, dict):
|
||||
return False
|
||||
return any(
|
||||
key == "" or value == "" or has_empty_keys_or_values(value)
|
||||
for key, value in _dict.items()
|
||||
)
|
||||
def put_has_empty_keys(field_updates, table):
|
||||
if table:
|
||||
key_names = table.key_attributes
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def get_empty_str_error():
|
||||
@ -257,7 +260,7 @@ class DynamoHandler(BaseResponse):
|
||||
er = "com.amazonaws.dynamodb.v20111205#ValidationException"
|
||||
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()
|
||||
|
||||
overwrite = "Expected" not in self.body
|
||||
@ -751,9 +754,6 @@ class DynamoHandler(BaseResponse):
|
||||
er = "com.amazonaws.dynamodb.v20111205#ValidationException"
|
||||
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:
|
||||
expected = self.body["Expected"]
|
||||
else:
|
||||
|
13
tests/test_dynamodb2/conftest.py
Normal file
13
tests/test_dynamodb2/conftest.py
Normal 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"},
|
||||
],
|
||||
)
|
@ -186,7 +186,7 @@ def test_list_not_found_table_tags():
|
||||
|
||||
@requires_boto_gte("2.9")
|
||||
@mock_dynamodb2
|
||||
def test_item_add_empty_string_exception():
|
||||
def test_item_add_empty_string_in_key_exception():
|
||||
name = "TestTable"
|
||||
conn = boto3.client(
|
||||
"dynamodb",
|
||||
@ -205,10 +205,10 @@ def test_item_add_empty_string_exception():
|
||||
conn.put_item(
|
||||
TableName=name,
|
||||
Item={
|
||||
"forum_name": {"S": "LOLCat Forum"},
|
||||
"forum_name": {"S": ""},
|
||||
"subject": {"S": "Check this out!"},
|
||||
"Body": {"S": "http://url_to_lolcat.gif"},
|
||||
"SentBy": {"S": ""},
|
||||
"SentBy": {"S": "someone@somewhere.edu"},
|
||||
"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")
|
||||
@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"
|
||||
conn = boto3.client(
|
||||
"dynamodb",
|
||||
@ -252,8 +281,8 @@ def test_update_item_with_empty_string_exception():
|
||||
conn.update_item(
|
||||
TableName=name,
|
||||
Key={"forum_name": {"S": "LOLCat Forum"}},
|
||||
UpdateExpression="set Body=:Body",
|
||||
ExpressionAttributeValues={":Body": {"S": ""}},
|
||||
UpdateExpression="set forum_name=:NewName",
|
||||
ExpressionAttributeValues={":NewName": {"S": ""}},
|
||||
)
|
||||
|
||||
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")
|
||||
@mock_dynamodb2
|
||||
def test_query_invalid_table():
|
||||
|
@ -7,7 +7,7 @@ from moto.dynamodb2.parsing.expressions import UpdateExpressionParser
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -22,6 +22,7 @@ def test_execution_of_if_not_exists_not_existing_value():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -34,7 +35,9 @@ def test_execution_of_if_not_exists_not_existing_value():
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
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_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -61,7 +65,7 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_attrib
|
||||
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_values = {":val": {"N": "4"}}
|
||||
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_values=update_expression_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -89,7 +94,9 @@ def test_execution_of_if_not_exists_with_existing_attribute_should_return_value(
|
||||
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_values = {":val": {"N": "4"}}
|
||||
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_values=update_expression_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
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
|
||||
|
||||
|
||||
def test_execution_of_sum_operation():
|
||||
def test_execution_of_sum_operation(table):
|
||||
update_expression = "SET a = a + b"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -132,6 +140,7 @@ def test_execution_of_sum_operation():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -144,7 +153,7 @@ def test_execution_of_sum_operation():
|
||||
assert expected_item == item
|
||||
|
||||
|
||||
def test_execution_of_remove():
|
||||
def test_execution_of_remove(table):
|
||||
update_expression = "Remove a"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -159,6 +168,7 @@ def test_execution_of_remove():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -171,7 +181,7 @@ def test_execution_of_remove():
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -198,6 +208,7 @@ def test_execution_of_remove_in_map():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -222,7 +233,7 @@ def test_execution_of_remove_in_map():
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -249,6 +260,7 @@ def test_execution_of_remove_in_list():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -270,7 +282,7 @@ def test_execution_of_remove_in_list():
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -285,6 +297,7 @@ def test_execution_of_delete_element_from_set():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":value": {"SS": ["value2", "value5"]}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -297,7 +310,7 @@ def test_execution_of_delete_element_from_set():
|
||||
assert expected_item == item
|
||||
|
||||
|
||||
def test_execution_of_add_number():
|
||||
def test_execution_of_add_number(table):
|
||||
update_expression = "add s :value"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -312,6 +325,7 @@ def test_execution_of_add_number():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":value": {"N": "10"}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -324,7 +338,7 @@ def test_execution_of_add_number():
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -340,6 +354,7 @@ def test_execution_of_add_set_to_a_number():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":value": {"SS": ["s1"]}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
expected_item = Item(
|
||||
@ -355,7 +370,7 @@ def test_execution_of_add_set_to_a_number():
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -370,6 +385,7 @@ def test_execution_of_add_to_a_set():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":value": {"SS": ["value2", "value5"]}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
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(
|
||||
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."""
|
||||
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_values=expression_attribute_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
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
|
||||
|
||||
|
||||
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."""
|
||||
update_expression = "delete s :value"
|
||||
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_values={":value": {"SS": ["value2"]}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
UpdateExpressionExecutor(validated_ast, item, None).execute()
|
||||
assert False, "Must raise exception"
|
||||
|
@ -7,6 +7,7 @@ from moto.dynamodb2.exceptions import (
|
||||
ExpressionAttributeNameNotDefined,
|
||||
IncorrectOperandType,
|
||||
InvalidUpdateExpressionInvalidDocumentPath,
|
||||
EmptyKeyAttributeException,
|
||||
)
|
||||
from moto.dynamodb2.models import Item, DynamoType
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
update_expression = "SET myNum = path + :val"
|
||||
update_expression_values = {":val": {"N": "3"}}
|
||||
@ -35,6 +57,7 @@ def test_validation_of_update_expression_with_keyword():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=update_expression_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
except AttributeIsReservedKeyword as e:
|
||||
@ -44,7 +67,9 @@ def test_validation_of_update_expression_with_keyword():
|
||||
@pytest.mark.parametrize(
|
||||
"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.
|
||||
|
||||
@ -65,12 +90,15 @@ def test_validation_of_a_set_statement_with_incorrect_passed_value(update_expres
|
||||
expression_attribute_names={"#b": "ok"},
|
||||
expression_attribute_values={":val": {"N": "3"}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
except ExpressionAttributeValueNotDefined as e:
|
||||
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.
|
||||
|
||||
@ -92,6 +120,7 @@ def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_i
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
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",])
|
||||
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.
|
||||
@ -122,13 +151,14 @@ def test_validation_of_update_expression_with_attribute_name_that_is_not_defined
|
||||
expression_attribute_names={"#b": "ok"},
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
except ExpressionAttributeNameNotDefined as e:
|
||||
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:
|
||||
update_expression = "SET a = if_not_exists(b, a.c)"
|
||||
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_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
except AttributeDoesNotExist:
|
||||
@ -172,7 +203,7 @@ def get_set_action_value(ast):
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -187,12 +218,15 @@ def test_validation_of_if_not_exists_not_existing_value():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
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_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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_values = {":val": {"N": "4"}}
|
||||
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_values=update_expression_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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_values = {":val": {"N": "4"}}
|
||||
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_values=update_expression_values,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -269,12 +308,13 @@ def test_validation_of_sum_operation():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -289,6 +329,7 @@ def test_validation_homogeneous_list_append_function():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":vals": {"L": [{"S": "i3"}, {"S": "i4"}]}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -311,12 +352,13 @@ def test_validation_hetereogenous_list_append_function():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":vals": {"L": [{"N": "3"}]}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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:
|
||||
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_values={":vals": {"S": "N"}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
except IncorrectOperandType as e:
|
||||
assert e.operand_type == "S"
|
||||
assert e.operator_or_function == "list_append"
|
||||
|
||||
|
||||
def test_sum_with_incompatible_types():
|
||||
def test_sum_with_incompatible_types(table):
|
||||
"""
|
||||
Must error out:
|
||||
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_values={":val": {"S": "N"}, ":val2": {"N": "3"}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
except IncorrectOperandType as e:
|
||||
assert e.operand_type == "S"
|
||||
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_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
@ -388,12 +432,13 @@ def test_validation_of_subraction_operation():
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":val": {"N": "1"}, ":val2": {"N": "3"}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
assert dynamo_value == DynamoType({"N": "-2"})
|
||||
|
||||
|
||||
def test_cannot_index_into_a_string():
|
||||
def test_cannot_index_into_a_string(table):
|
||||
"""
|
||||
Must error out:
|
||||
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_values={":Item": {"S": "string_update"}},
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
assert False, "Must raise exception"
|
||||
except InvalidUpdateExpressionInvalidDocumentPath:
|
||||
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"""
|
||||
update_expression = "set d=a"
|
||||
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_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
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:
|
||||
update_expression = "set d.e=a"
|
||||
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_values=None,
|
||||
item=item,
|
||||
table=table,
|
||||
).validate()
|
||||
assert False, "Must raise exception"
|
||||
except InvalidUpdateExpressionInvalidDocumentPath:
|
||||
|
Loading…
Reference in New Issue
Block a user