Merge branch 'master' into feature/dynamodb_item_limit
This commit is contained in:
commit
8595493aee
53 changed files with 4651 additions and 122 deletions
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import unicode_literals, print_function
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
import six
|
||||
|
|
@ -1453,6 +1454,13 @@ def test_filter_expression():
|
|||
filter_expr.expr(row1).should.be(True)
|
||||
filter_expr.expr(row2).should.be(False)
|
||||
|
||||
# lowercase AND test
|
||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression(
|
||||
"Id > :v0 and Subs < :v1", {}, {":v0": {"N": "5"}, ":v1": {"N": "7"}}
|
||||
)
|
||||
filter_expr.expr(row1).should.be(True)
|
||||
filter_expr.expr(row2).should.be(False)
|
||||
|
||||
# OR test
|
||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression(
|
||||
"Id = :v0 OR Id=:v1", {}, {":v0": {"N": "5"}, ":v1": {"N": "8"}}
|
||||
|
|
@ -2146,13 +2154,33 @@ def test_update_item_on_map():
|
|||
# Nonexistent nested attributes are supported for existing top-level attributes.
|
||||
table.update_item(
|
||||
Key={"forum_name": "the-key", "subject": "123"},
|
||||
UpdateExpression="SET body.#nested.#data = :tb, body.nested.#nonexistentnested.#data = :tb2",
|
||||
UpdateExpression="SET body.#nested.#data = :tb",
|
||||
ExpressionAttributeNames={"#nested": "nested", "#data": "data",},
|
||||
ExpressionAttributeValues={":tb": "new_value"},
|
||||
)
|
||||
# Running this against AWS DDB gives an exception so make sure it also fails.:
|
||||
with assert_raises(client.exceptions.ClientError):
|
||||
# botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the UpdateItem
|
||||
# operation: The document path provided in the update expression is invalid for update
|
||||
table.update_item(
|
||||
Key={"forum_name": "the-key", "subject": "123"},
|
||||
UpdateExpression="SET body.#nested.#nonexistentnested.#data = :tb2",
|
||||
ExpressionAttributeNames={
|
||||
"#nested": "nested",
|
||||
"#nonexistentnested": "nonexistentnested",
|
||||
"#data": "data",
|
||||
},
|
||||
ExpressionAttributeValues={":tb2": "other_value"},
|
||||
)
|
||||
|
||||
table.update_item(
|
||||
Key={"forum_name": "the-key", "subject": "123"},
|
||||
UpdateExpression="SET body.#nested.#nonexistentnested = :tb2",
|
||||
ExpressionAttributeNames={
|
||||
"#nested": "nested",
|
||||
"#nonexistentnested": "nonexistentnested",
|
||||
"#data": "data",
|
||||
},
|
||||
ExpressionAttributeValues={":tb": "new_value", ":tb2": "other_value"},
|
||||
ExpressionAttributeValues={":tb2": {"data": "other_value"}},
|
||||
)
|
||||
|
||||
resp = table.scan()
|
||||
|
|
@ -2160,8 +2188,8 @@ def test_update_item_on_map():
|
|||
{"nested": {"data": "new_value", "nonexistentnested": {"data": "other_value"}}}
|
||||
)
|
||||
|
||||
# Test nested value for a nonexistent attribute.
|
||||
with assert_raises(client.exceptions.ConditionalCheckFailedException):
|
||||
# Test nested value for a nonexistent attribute throws a ClientError.
|
||||
with assert_raises(client.exceptions.ClientError):
|
||||
table.update_item(
|
||||
Key={"forum_name": "the-key", "subject": "123"},
|
||||
UpdateExpression="SET nonexistent.#nested = :tb",
|
||||
|
|
@ -2764,7 +2792,7 @@ def test_query_gsi_with_range_key():
|
|||
res = dynamodb.query(
|
||||
TableName="test",
|
||||
IndexName="test_gsi",
|
||||
KeyConditionExpression="gsi_hash_key = :gsi_hash_key AND gsi_range_key = :gsi_range_key",
|
||||
KeyConditionExpression="gsi_hash_key = :gsi_hash_key and gsi_range_key = :gsi_range_key",
|
||||
ExpressionAttributeValues={
|
||||
":gsi_hash_key": {"S": "key1"},
|
||||
":gsi_range_key": {"S": "range1"},
|
||||
|
|
@ -3183,7 +3211,10 @@ def test_remove_top_level_attribute():
|
|||
TableName=table_name, Item={"id": {"S": "foo"}, "item": {"S": "bar"}}
|
||||
)
|
||||
client.update_item(
|
||||
TableName=table_name, Key={"id": {"S": "foo"}}, UpdateExpression="REMOVE item"
|
||||
TableName=table_name,
|
||||
Key={"id": {"S": "foo"}},
|
||||
UpdateExpression="REMOVE #i",
|
||||
ExpressionAttributeNames={"#i": "item"},
|
||||
)
|
||||
#
|
||||
result = client.get_item(TableName=table_name, Key={"id": {"S": "foo"}})["Item"]
|
||||
|
|
@ -3358,21 +3389,21 @@ def test_item_size_is_under_400KB():
|
|||
assert_failure_due_to_item_size(
|
||||
func=client.put_item,
|
||||
TableName="moto-test",
|
||||
Item={"id": {"S": "foo"}, "item": {"S": large_item}},
|
||||
Item={"id": {"S": "foo"}, "cont": {"S": large_item}},
|
||||
)
|
||||
assert_failure_due_to_item_size(
|
||||
func=table.put_item, Item={"id": "bar", "item": large_item}
|
||||
func=table.put_item, Item={"id": "bar", "cont": large_item}
|
||||
)
|
||||
assert_failure_due_to_item_size(
|
||||
assert_failure_due_to_item_size_to_update(
|
||||
func=client.update_item,
|
||||
TableName="moto-test",
|
||||
Key={"id": {"S": "foo2"}},
|
||||
UpdateExpression="set item=:Item",
|
||||
UpdateExpression="set cont=:Item",
|
||||
ExpressionAttributeValues={":Item": {"S": large_item}},
|
||||
)
|
||||
# Assert op fails when updating a nested item
|
||||
assert_failure_due_to_item_size(
|
||||
func=table.put_item, Item={"id": "bar", "itemlist": [{"item": large_item}]}
|
||||
func=table.put_item, Item={"id": "bar", "itemlist": [{"cont": large_item}]}
|
||||
)
|
||||
assert_failure_due_to_item_size(
|
||||
func=client.put_item,
|
||||
|
|
@ -3393,6 +3424,15 @@ def assert_failure_due_to_item_size(func, **kwargs):
|
|||
)
|
||||
|
||||
|
||||
def assert_failure_due_to_item_size_to_update(func, **kwargs):
|
||||
with assert_raises(ClientError) as ex:
|
||||
func(**kwargs)
|
||||
ex.exception.response["Error"]["Code"].should.equal("ValidationException")
|
||||
ex.exception.response["Error"]["Message"].should.equal(
|
||||
"Item size to update has exceeded the maximum allowed size"
|
||||
)
|
||||
|
||||
|
||||
@mock_dynamodb2
|
||||
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression
|
||||
def test_hash_key_cannot_use_begins_with_operations():
|
||||
|
|
@ -4215,3 +4255,79 @@ def test_dynamodb_max_1mb_limit():
|
|||
# We shouldn't get everything back - the total result set is well over 1MB
|
||||
len(items).should.be.greater_than(response["Count"])
|
||||
response["LastEvaluatedKey"].shouldnt.be(None)
|
||||
|
||||
|
||||
def assert_raise_syntax_error(client_error, token, near):
|
||||
"""
|
||||
Assert whether a client_error is as expected Syntax error. Syntax error looks like: `syntax_error_template`
|
||||
|
||||
Args:
|
||||
client_error(ClientError): The ClientError exception that was raised
|
||||
token(str): The token that ws unexpected
|
||||
near(str): The part in the expression that shows where the error occurs it generally has the preceding token the
|
||||
optional separation and the problematic token.
|
||||
"""
|
||||
syntax_error_template = (
|
||||
'Invalid UpdateExpression: Syntax error; token: "{token}", near: "{near}"'
|
||||
)
|
||||
expected_syntax_error = syntax_error_template.format(token=token, near=near)
|
||||
assert client_error.response["Error"]["Code"] == "ValidationException"
|
||||
assert expected_syntax_error == client_error.response["Error"]["Message"]
|
||||
|
||||
|
||||
@mock_dynamodb2
|
||||
def test_update_expression_with_numeric_literal_instead_of_value():
|
||||
"""
|
||||
DynamoDB requires literals to be passed in as values. If they are put literally in the expression a token error will
|
||||
be raised
|
||||
"""
|
||||
dynamodb = boto3.client("dynamodb", region_name="eu-west-1")
|
||||
|
||||
dynamodb.create_table(
|
||||
TableName="moto-test",
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
)
|
||||
|
||||
try:
|
||||
dynamodb.update_item(
|
||||
TableName="moto-test",
|
||||
Key={"id": {"S": "1"}},
|
||||
UpdateExpression="SET MyStr = myNum + 1",
|
||||
)
|
||||
assert False, "Validation exception not thrown"
|
||||
except dynamodb.exceptions.ClientError as e:
|
||||
assert_raise_syntax_error(e, "1", "+ 1")
|
||||
|
||||
|
||||
@mock_dynamodb2
|
||||
def test_update_expression_with_multiple_set_clauses_must_be_comma_separated():
|
||||
"""
|
||||
An UpdateExpression can have multiple set clauses but if they are passed in without the separating comma.
|
||||
"""
|
||||
dynamodb = boto3.client("dynamodb", region_name="eu-west-1")
|
||||
|
||||
dynamodb.create_table(
|
||||
TableName="moto-test",
|
||||
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
|
||||
)
|
||||
|
||||
try:
|
||||
dynamodb.update_item(
|
||||
TableName="moto-test",
|
||||
Key={"id": {"S": "1"}},
|
||||
UpdateExpression="SET MyStr = myNum Mystr2 myNum2",
|
||||
)
|
||||
assert False, "Validation exception not thrown"
|
||||
except dynamodb.exceptions.ClientError as e:
|
||||
assert_raise_syntax_error(e, "Mystr2", "myNum Mystr2 myNum2")
|
||||
|
||||
|
||||
@mock_dynamodb2
|
||||
def test_list_tables_exclusive_start_table_name_empty():
|
||||
client = boto3.client("dynamodb", region_name="us-east-1")
|
||||
|
||||
resp = client.list_tables(Limit=1, ExclusiveStartTableName="whatever")
|
||||
|
||||
len(resp["TableNames"]).should.equal(0)
|
||||
|
|
|
|||
259
tests/test_dynamodb2/test_dynamodb_expression_tokenizer.py
Normal file
259
tests/test_dynamodb2/test_dynamodb_expression_tokenizer.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
from moto.dynamodb2.exceptions import (
|
||||
InvalidTokenException,
|
||||
InvalidExpressionAttributeNameKey,
|
||||
)
|
||||
from moto.dynamodb2.parsing.tokens import ExpressionTokenizer, Token
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action():
|
||||
set_action = "SET attrName = :attrValue"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attrValue"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_leading_space():
|
||||
set_action = "Set attrName = :attrValue"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "Set"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attrValue"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_attribute_name_leading_space():
|
||||
set_action = "SET #a = :attrValue"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_NAME, "#a"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attrValue"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_trailing_space():
|
||||
set_action = "SET attrName = :attrValue "
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attrValue"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_multi_spaces():
|
||||
set_action = "SET attrName = :attrValue "
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attrValue"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_with_numbers_in_identifiers():
|
||||
set_action = "SET attrName3 = :attr3Value"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName3"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attr3Value"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_with_underscore_in_identifier():
|
||||
set_action = "SET attr_Name = :attr_Value"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attr_Name"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attr_Value"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_leading_underscore_in_attribute_name_expression():
|
||||
"""Leading underscore is not allowed for an attribute name"""
|
||||
set_action = "SET attrName = _idid"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "_"
|
||||
assert te.near == "= _idid"
|
||||
|
||||
|
||||
def test_expression_tokenizer_leading_underscore_in_attribute_value_expression():
|
||||
"""Leading underscore is allowed in an attribute value"""
|
||||
set_action = "SET attrName = :_attrValue"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":_attrValue"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_nested_attribute():
|
||||
set_action = "SET attrName.elem = :attrValue"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attrName"),
|
||||
Token(Token.DOT, "."),
|
||||
Token(Token.ATTRIBUTE, "elem"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":attrValue"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_list_index_with_sub_attribute():
|
||||
set_action = "SET itemmap.itemlist[1].foos=:Item"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "itemmap"),
|
||||
Token(Token.DOT, "."),
|
||||
Token(Token.ATTRIBUTE, "itemlist"),
|
||||
Token(Token.OPEN_SQUARE_BRACKET, "["),
|
||||
Token(Token.NUMBER, "1"),
|
||||
Token(Token.CLOSE_SQUARE_BRACKET, "]"),
|
||||
Token(Token.DOT, "."),
|
||||
Token(Token.ATTRIBUTE, "foos"),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":Item"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_list_index_surrounded_with_whitespace():
|
||||
set_action = "SET itemlist[ 1 ]=:Item"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "itemlist"),
|
||||
Token(Token.OPEN_SQUARE_BRACKET, "["),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.NUMBER, "1"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.CLOSE_SQUARE_BRACKET, "]"),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.ATTRIBUTE_VALUE, ":Item"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_attribute_name_invalid_key():
|
||||
"""
|
||||
ExpressionAttributeNames contains invalid key: Syntax error; key: "#va#l2"
|
||||
"""
|
||||
set_action = "SET #va#l2 = 3"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidExpressionAttributeNameKey as e:
|
||||
assert e.key == "#va#l2"
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_attribute_name_invalid_key_double_hash():
|
||||
"""
|
||||
ExpressionAttributeNames contains invalid key: Syntax error; key: "#va#l"
|
||||
"""
|
||||
set_action = "SET #va#l = 3"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidExpressionAttributeNameKey as e:
|
||||
assert e.key == "#va#l"
|
||||
|
||||
|
||||
def test_expression_tokenizer_single_set_action_attribute_name_valid_key():
|
||||
set_action = "SET attr=#val2"
|
||||
token_list = ExpressionTokenizer.make_list(set_action)
|
||||
assert token_list == [
|
||||
Token(Token.ATTRIBUTE, "SET"),
|
||||
Token(Token.WHITESPACE, " "),
|
||||
Token(Token.ATTRIBUTE, "attr"),
|
||||
Token(Token.EQUAL_SIGN, "="),
|
||||
Token(Token.ATTRIBUTE_NAME, "#val2"),
|
||||
]
|
||||
|
||||
|
||||
def test_expression_tokenizer_just_a_pipe():
|
||||
set_action = "|"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "|"
|
||||
assert te.near == "|"
|
||||
|
||||
|
||||
def test_expression_tokenizer_just_a_pipe_with_leading_white_spaces():
|
||||
set_action = " |"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "|"
|
||||
assert te.near == " |"
|
||||
|
||||
|
||||
def test_expression_tokenizer_just_a_pipe_for_set_expression():
|
||||
set_action = "SET|"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "|"
|
||||
assert te.near == "SET|"
|
||||
|
||||
|
||||
def test_expression_tokenizer_just_an_attribute_and_a_pipe_for_set_expression():
|
||||
set_action = "SET a|"
|
||||
try:
|
||||
ExpressionTokenizer.make_list(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "|"
|
||||
assert te.near == "a|"
|
||||
405
tests/test_dynamodb2/test_dynamodb_expressions.py
Normal file
405
tests/test_dynamodb2/test_dynamodb_expressions.py
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
from moto.dynamodb2.exceptions import InvalidTokenException
|
||||
from moto.dynamodb2.parsing.expressions import UpdateExpressionParser
|
||||
from moto.dynamodb2.parsing.reserved_keywords import ReservedKeywords
|
||||
|
||||
|
||||
def test_get_reserved_keywords():
|
||||
reserved_keywords = ReservedKeywords.get_reserved_keywords()
|
||||
assert "SET" in reserved_keywords
|
||||
assert "DELETE" in reserved_keywords
|
||||
assert "ADD" in reserved_keywords
|
||||
# REMOVE is not part of the list of reserved keywords.
|
||||
assert "REMOVE" not in reserved_keywords
|
||||
|
||||
|
||||
def test_update_expression_numeric_literal_in_expression():
|
||||
set_action = "SET attrName = 3"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "3"
|
||||
assert te.near == "= 3"
|
||||
|
||||
|
||||
def test_expression_tokenizer_multi_number_numeric_literal_in_expression():
|
||||
set_action = "SET attrName = 34"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "34"
|
||||
assert te.near == "= 34"
|
||||
|
||||
|
||||
def test_expression_tokenizer_numeric_literal_unclosed_square_bracket():
|
||||
set_action = "SET MyStr[ 3"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "<EOF>"
|
||||
assert te.near == "3"
|
||||
|
||||
|
||||
def test_expression_tokenizer_wrong_closing_bracket_with_space():
|
||||
set_action = "SET MyStr[3 )"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == ")"
|
||||
assert te.near == "3 )"
|
||||
|
||||
|
||||
def test_expression_tokenizer_wrong_closing_bracket():
|
||||
set_action = "SET MyStr[3)"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == ")"
|
||||
assert te.near == "3)"
|
||||
|
||||
|
||||
def test_expression_tokenizer_only_numeric_literal_for_set():
|
||||
set_action = "SET 2"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "2"
|
||||
assert te.near == "SET 2"
|
||||
|
||||
|
||||
def test_expression_tokenizer_only_numeric_literal():
|
||||
set_action = "2"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "2"
|
||||
assert te.near == "2"
|
||||
|
||||
|
||||
def test_expression_tokenizer_set_closing_round_bracket():
|
||||
set_action = "SET )"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == ")"
|
||||
assert te.near == "SET )"
|
||||
|
||||
|
||||
def test_expression_tokenizer_set_closing_followed_by_numeric_literal():
|
||||
set_action = "SET ) 3"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == ")"
|
||||
assert te.near == "SET ) 3"
|
||||
|
||||
|
||||
def test_expression_tokenizer_numeric_literal_unclosed_square_bracket_trailing_space():
|
||||
set_action = "SET MyStr[ 3 "
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "<EOF>"
|
||||
assert te.near == "3 "
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_round_brackets_only_opening():
|
||||
set_action = "SET MyStr = (:_val"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "<EOF>"
|
||||
assert te.near == ":_val"
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_round_brackets_only_opening_trailing_space():
|
||||
set_action = "SET MyStr = (:_val "
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "<EOF>"
|
||||
assert te.near == ":_val "
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_square_brackets_only_opening():
|
||||
set_action = "SET MyStr = [:_val"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "["
|
||||
assert te.near == "= [:_val"
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_square_brackets_only_opening_trailing_spaces():
|
||||
set_action = "SET MyStr = [:_val "
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "["
|
||||
assert te.near == "= [:_val"
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_round_brackets_multiple_opening():
|
||||
set_action = "SET MyStr = (:_val + (:val2"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "<EOF>"
|
||||
assert te.near == ":val2"
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_round_brackets_only_closing():
|
||||
set_action = "SET MyStr = ):_val"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == ")"
|
||||
assert te.near == "= ):_val"
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_square_brackets_only_closing():
|
||||
set_action = "SET MyStr = ]:_val"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "]"
|
||||
assert te.near == "= ]:_val"
|
||||
|
||||
|
||||
def test_expression_tokenizer_unbalanced_round_brackets_only_closing_followed_by_other_parts():
|
||||
set_action = "SET MyStr = ):_val + :val2"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == ")"
|
||||
assert te.near == "= ):_val"
|
||||
|
||||
|
||||
def test_update_expression_starts_with_keyword_reset_followed_by_identifier():
|
||||
update_expression = "RESET NonExistent"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "RESET"
|
||||
assert te.near == "RESET NonExistent"
|
||||
|
||||
|
||||
def test_update_expression_starts_with_keyword_reset_followed_by_identifier_and_value():
|
||||
update_expression = "RESET NonExistent value"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "RESET"
|
||||
assert te.near == "RESET NonExistent"
|
||||
|
||||
|
||||
def test_update_expression_starts_with_leading_spaces_and_keyword_reset_followed_by_identifier_and_value():
|
||||
update_expression = " RESET NonExistent value"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "RESET"
|
||||
assert te.near == " RESET NonExistent"
|
||||
|
||||
|
||||
def test_update_expression_with_only_keyword_reset():
|
||||
update_expression = "RESET"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "RESET"
|
||||
assert te.near == "RESET"
|
||||
|
||||
|
||||
def test_update_nested_expression_with_selector_just_should_fail_parsing_at_numeric_literal_value():
|
||||
update_expression = "SET a[0].b = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "5"
|
||||
assert te.near == "= 5"
|
||||
|
||||
|
||||
def test_update_nested_expression_with_selector_and_spaces_should_only_fail_parsing_at_numeric_literal_value():
|
||||
update_expression = "SET a [ 2 ]. b = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "5"
|
||||
assert te.near == "= 5"
|
||||
|
||||
|
||||
def test_update_nested_expression_with_double_selector_and_spaces_should_only_fail_parsing_at_numeric_literal_value():
|
||||
update_expression = "SET a [2][ 3 ]. b = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "5"
|
||||
assert te.near == "= 5"
|
||||
|
||||
|
||||
def test_update_nested_expression_should_only_fail_parsing_at_numeric_literal_value():
|
||||
update_expression = "SET a . b = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "5"
|
||||
assert te.near == "= 5"
|
||||
|
||||
|
||||
def test_nested_selectors_in_update_expression_should_fail_at_nesting():
|
||||
update_expression = "SET a [ [2] ]. b = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "["
|
||||
assert te.near == "[ [2"
|
||||
|
||||
|
||||
def test_update_expression_number_in_selector_cannot_be_splite():
|
||||
update_expression = "SET a [2 1]. b = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "1"
|
||||
assert te.near == "2 1]"
|
||||
|
||||
|
||||
def test_update_expression_cannot_have_successive_attributes():
|
||||
update_expression = "SET #a a = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "a"
|
||||
assert te.near == "#a a ="
|
||||
|
||||
|
||||
def test_update_expression_path_with_both_attribute_and_attribute_name_should_only_fail_at_numeric_value():
|
||||
update_expression = "SET #a.a = 5"
|
||||
try:
|
||||
UpdateExpressionParser.make(update_expression)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "5"
|
||||
assert te.near == "= 5"
|
||||
|
||||
|
||||
def test_expression_tokenizer_2_same_operators_back_to_back():
|
||||
set_action = "SET MyStr = NoExist + + :_val "
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "+"
|
||||
assert te.near == "+ + :_val"
|
||||
|
||||
|
||||
def test_expression_tokenizer_2_different_operators_back_to_back():
|
||||
set_action = "SET MyStr = NoExist + - :_val "
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "-"
|
||||
assert te.near == "+ - :_val"
|
||||
|
||||
|
||||
def test_update_expression_remove_does_not_allow_operations():
|
||||
remove_action = "REMOVE NoExist + "
|
||||
try:
|
||||
UpdateExpressionParser.make(remove_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "+"
|
||||
assert te.near == "NoExist + "
|
||||
|
||||
|
||||
def test_update_expression_add_does_not_allow_attribute_after_path():
|
||||
"""value here is not really a value since a value starts with a colon (:)"""
|
||||
add_expr = "ADD attr val foobar"
|
||||
try:
|
||||
UpdateExpressionParser.make(add_expr)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "val"
|
||||
assert te.near == "attr val foobar"
|
||||
|
||||
|
||||
def test_update_expression_add_does_not_allow_attribute_foobar_after_value():
|
||||
add_expr = "ADD attr :val foobar"
|
||||
try:
|
||||
UpdateExpressionParser.make(add_expr)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "foobar"
|
||||
assert te.near == ":val foobar"
|
||||
|
||||
|
||||
def test_update_expression_delete_does_not_allow_attribute_after_path():
|
||||
"""value here is not really a value since a value starts with a colon (:)"""
|
||||
delete_expr = "DELETE attr val"
|
||||
try:
|
||||
UpdateExpressionParser.make(delete_expr)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "val"
|
||||
assert te.near == "attr val"
|
||||
|
||||
|
||||
def test_update_expression_delete_does_not_allow_attribute_foobar_after_value():
|
||||
delete_expr = "DELETE attr :val foobar"
|
||||
try:
|
||||
UpdateExpressionParser.make(delete_expr)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "foobar"
|
||||
assert te.near == ":val foobar"
|
||||
|
||||
|
||||
def test_update_expression_parsing_is_not_keyword_aware():
|
||||
"""path and VALUE are keywords. Yet a token error will be thrown for the numeric literal 1."""
|
||||
delete_expr = "SET path = VALUE 1"
|
||||
try:
|
||||
UpdateExpressionParser.make(delete_expr)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "1"
|
||||
assert te.near == "VALUE 1"
|
||||
|
||||
|
||||
def test_expression_if_not_exists_is_not_valid_in_remove_statement():
|
||||
set_action = "REMOVE if_not_exists(a,b)"
|
||||
try:
|
||||
UpdateExpressionParser.make(set_action)
|
||||
assert False, "Exception not raised correctly"
|
||||
except InvalidTokenException as te:
|
||||
assert te.token == "("
|
||||
assert te.near == "if_not_exists(a"
|
||||
|
|
@ -1254,14 +1254,22 @@ def test_update_item_with_expression():
|
|||
|
||||
item_key = {"forum_name": "the-key", "subject": "123"}
|
||||
|
||||
table.update_item(Key=item_key, UpdateExpression="SET field=2")
|
||||
table.update_item(
|
||||
Key=item_key,
|
||||
UpdateExpression="SET field = :field_value",
|
||||
ExpressionAttributeValues={":field_value": 2},
|
||||
)
|
||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(
|
||||
{"field": "2", "forum_name": "the-key", "subject": "123"}
|
||||
{"field": Decimal("2"), "forum_name": "the-key", "subject": "123"}
|
||||
)
|
||||
|
||||
table.update_item(Key=item_key, UpdateExpression="SET field = 3")
|
||||
table.update_item(
|
||||
Key=item_key,
|
||||
UpdateExpression="SET field = :field_value",
|
||||
ExpressionAttributeValues={":field_value": 3},
|
||||
)
|
||||
dict(table.get_item(Key=item_key)["Item"]).should.equal(
|
||||
{"field": "3", "forum_name": "the-key", "subject": "123"}
|
||||
{"field": Decimal("3"), "forum_name": "the-key", "subject": "123"}
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -443,23 +443,40 @@ def test_update_item_nested_remove():
|
|||
dict(returned_item).should.equal({"username": "steve", "Meta": {}})
|
||||
|
||||
|
||||
@mock_dynamodb2_deprecated
|
||||
@mock_dynamodb2
|
||||
def test_update_item_double_nested_remove():
|
||||
conn = boto.dynamodb2.connect_to_region("us-east-1")
|
||||
table = Table.create("messages", schema=[HashKey("username")])
|
||||
conn = boto3.client("dynamodb", region_name="us-east-1")
|
||||
conn.create_table(
|
||||
TableName="messages",
|
||||
KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
|
||||
AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
|
||||
BillingMode="PAY_PER_REQUEST",
|
||||
)
|
||||
|
||||
data = {"username": "steve", "Meta": {"Name": {"First": "Steve", "Last": "Urkel"}}}
|
||||
table.put_item(data=data)
|
||||
item = {
|
||||
"username": {"S": "steve"},
|
||||
"Meta": {
|
||||
"M": {"Name": {"M": {"First": {"S": "Steve"}, "Last": {"S": "Urkel"}}}}
|
||||
},
|
||||
}
|
||||
conn.put_item(TableName="messages", Item=item)
|
||||
key_map = {"username": {"S": "steve"}}
|
||||
|
||||
# Then remove the Meta.FullName field
|
||||
conn.update_item("messages", key_map, update_expression="REMOVE Meta.Name.First")
|
||||
|
||||
returned_item = table.get_item(username="steve")
|
||||
dict(returned_item).should.equal(
|
||||
{"username": "steve", "Meta": {"Name": {"Last": "Urkel"}}}
|
||||
conn.update_item(
|
||||
TableName="messages",
|
||||
Key=key_map,
|
||||
UpdateExpression="REMOVE Meta.#N.#F",
|
||||
ExpressionAttributeNames={"#N": "Name", "#F": "First"},
|
||||
)
|
||||
|
||||
returned_item = conn.get_item(TableName="messages", Key=key_map)
|
||||
expected_item = {
|
||||
"username": {"S": "steve"},
|
||||
"Meta": {"M": {"Name": {"M": {"Last": {"S": "Urkel"}}}}},
|
||||
}
|
||||
dict(returned_item["Item"]).should.equal(expected_item)
|
||||
|
||||
|
||||
@mock_dynamodb2_deprecated
|
||||
def test_update_item_set():
|
||||
|
|
@ -471,7 +488,10 @@ def test_update_item_set():
|
|||
key_map = {"username": {"S": "steve"}}
|
||||
|
||||
conn.update_item(
|
||||
"messages", key_map, update_expression="SET foo=bar, blah=baz REMOVE SentBy"
|
||||
"messages",
|
||||
key_map,
|
||||
update_expression="SET foo=:bar, blah=:baz REMOVE SentBy",
|
||||
expression_attribute_values={":bar": {"S": "bar"}, ":baz": {"S": "baz"}},
|
||||
)
|
||||
|
||||
returned_item = table.get_item(username="steve")
|
||||
|
|
@ -616,8 +636,9 @@ def test_boto3_update_item_conditions_fail():
|
|||
table.put_item(Item={"username": "johndoe", "foo": "baz"})
|
||||
table.update_item.when.called_with(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=bar",
|
||||
UpdateExpression="SET foo=:bar",
|
||||
Expected={"foo": {"Value": "bar"}},
|
||||
ExpressionAttributeValues={":bar": "bar"},
|
||||
).should.throw(botocore.client.ClientError)
|
||||
|
||||
|
||||
|
|
@ -627,8 +648,9 @@ def test_boto3_update_item_conditions_fail_because_expect_not_exists():
|
|||
table.put_item(Item={"username": "johndoe", "foo": "baz"})
|
||||
table.update_item.when.called_with(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=bar",
|
||||
UpdateExpression="SET foo=:bar",
|
||||
Expected={"foo": {"Exists": False}},
|
||||
ExpressionAttributeValues={":bar": "bar"},
|
||||
).should.throw(botocore.client.ClientError)
|
||||
|
||||
|
||||
|
|
@ -638,8 +660,9 @@ def test_boto3_update_item_conditions_fail_because_expect_not_exists_by_compare_
|
|||
table.put_item(Item={"username": "johndoe", "foo": "baz"})
|
||||
table.update_item.when.called_with(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=bar",
|
||||
UpdateExpression="SET foo=:bar",
|
||||
Expected={"foo": {"ComparisonOperator": "NULL"}},
|
||||
ExpressionAttributeValues={":bar": "bar"},
|
||||
).should.throw(botocore.client.ClientError)
|
||||
|
||||
|
||||
|
|
@ -649,8 +672,9 @@ def test_boto3_update_item_conditions_pass():
|
|||
table.put_item(Item={"username": "johndoe", "foo": "bar"})
|
||||
table.update_item(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=baz",
|
||||
UpdateExpression="SET foo=:baz",
|
||||
Expected={"foo": {"Value": "bar"}},
|
||||
ExpressionAttributeValues={":baz": "baz"},
|
||||
)
|
||||
returned_item = table.get_item(Key={"username": "johndoe"})
|
||||
assert dict(returned_item)["Item"]["foo"].should.equal("baz")
|
||||
|
|
@ -662,8 +686,9 @@ def test_boto3_update_item_conditions_pass_because_expect_not_exists():
|
|||
table.put_item(Item={"username": "johndoe", "foo": "bar"})
|
||||
table.update_item(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=baz",
|
||||
UpdateExpression="SET foo=:baz",
|
||||
Expected={"whatever": {"Exists": False}},
|
||||
ExpressionAttributeValues={":baz": "baz"},
|
||||
)
|
||||
returned_item = table.get_item(Key={"username": "johndoe"})
|
||||
assert dict(returned_item)["Item"]["foo"].should.equal("baz")
|
||||
|
|
@ -675,8 +700,9 @@ def test_boto3_update_item_conditions_pass_because_expect_not_exists_by_compare_
|
|||
table.put_item(Item={"username": "johndoe", "foo": "bar"})
|
||||
table.update_item(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=baz",
|
||||
UpdateExpression="SET foo=:baz",
|
||||
Expected={"whatever": {"ComparisonOperator": "NULL"}},
|
||||
ExpressionAttributeValues={":baz": "baz"},
|
||||
)
|
||||
returned_item = table.get_item(Key={"username": "johndoe"})
|
||||
assert dict(returned_item)["Item"]["foo"].should.equal("baz")
|
||||
|
|
@ -688,8 +714,9 @@ def test_boto3_update_item_conditions_pass_because_expect_exists_by_compare_to_n
|
|||
table.put_item(Item={"username": "johndoe", "foo": "bar"})
|
||||
table.update_item(
|
||||
Key={"username": "johndoe"},
|
||||
UpdateExpression="SET foo=baz",
|
||||
UpdateExpression="SET foo=:baz",
|
||||
Expected={"foo": {"ComparisonOperator": "NOT_NULL"}},
|
||||
ExpressionAttributeValues={":baz": "baz"},
|
||||
)
|
||||
returned_item = table.get_item(Key={"username": "johndoe"})
|
||||
assert dict(returned_item)["Item"]["foo"].should.equal("baz")
|
||||
|
|
|
|||
464
tests/test_dynamodb2/test_dynamodb_validation.py
Normal file
464
tests/test_dynamodb2/test_dynamodb_validation.py
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
from moto.dynamodb2.exceptions import (
|
||||
AttributeIsReservedKeyword,
|
||||
ExpressionAttributeValueNotDefined,
|
||||
AttributeDoesNotExist,
|
||||
ExpressionAttributeNameNotDefined,
|
||||
IncorrectOperandType,
|
||||
InvalidUpdateExpressionInvalidDocumentPath,
|
||||
)
|
||||
from moto.dynamodb2.models import Item, DynamoType
|
||||
from moto.dynamodb2.parsing.ast_nodes import (
|
||||
NodeDepthLeftTypeFetcher,
|
||||
UpdateExpressionSetAction,
|
||||
UpdateExpressionValue,
|
||||
DDBTypedValue,
|
||||
)
|
||||
from moto.dynamodb2.parsing.expressions import UpdateExpressionParser
|
||||
from moto.dynamodb2.parsing.validators import UpdateExpressionValidator
|
||||
from parameterized import parameterized
|
||||
|
||||
|
||||
def test_validation_of_update_expression_with_keyword():
|
||||
try:
|
||||
update_expression = "SET myNum = path + :val"
|
||||
update_expression_values = {":val": {"N": "3"}}
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "path": {"N": "3"}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=update_expression_values,
|
||||
item=item,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
except AttributeIsReservedKeyword as e:
|
||||
assert e.keyword == "path"
|
||||
|
||||
|
||||
@parameterized(
|
||||
["SET a = #b + :val2", "SET a = :val2 + #b",]
|
||||
)
|
||||
def test_validation_of_a_set_statement_with_incorrect_passed_value(update_expression):
|
||||
"""
|
||||
By running permutations it shows that values are replaced prior to resolving attributes.
|
||||
|
||||
An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression:
|
||||
An expression attribute value used in expression is not defined; attribute value: :val2
|
||||
"""
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "b": {"N": "3"}},
|
||||
)
|
||||
try:
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names={"#b": "ok"},
|
||||
expression_attribute_values={":val": {"N": "3"}},
|
||||
item=item,
|
||||
).validate()
|
||||
except ExpressionAttributeValueNotDefined as e:
|
||||
assert e.attribute_value == ":val2"
|
||||
|
||||
|
||||
def test_validation_of_update_expression_with_attribute_that_does_not_exist_in_item():
|
||||
"""
|
||||
When an update expression tries to get an attribute that does not exist it must throw the appropriate exception.
|
||||
|
||||
An error occurred (ValidationException) when calling the UpdateItem operation:
|
||||
The provided expression refers to an attribute that does not exist in the item
|
||||
"""
|
||||
try:
|
||||
update_expression = "SET a = nonexistent"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "path": {"N": "3"}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
except AttributeDoesNotExist:
|
||||
assert True
|
||||
|
||||
|
||||
@parameterized(
|
||||
["SET a = #c", "SET a = #c + #d",]
|
||||
)
|
||||
def test_validation_of_update_expression_with_attribute_name_that_is_not_defined(
|
||||
update_expression,
|
||||
):
|
||||
"""
|
||||
When an update expression tries to get an attribute name that is not provided it must throw an exception.
|
||||
|
||||
An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression:
|
||||
An expression attribute name used in the document path is not defined; attribute name: #c
|
||||
"""
|
||||
try:
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "path": {"N": "3"}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names={"#b": "ok"},
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).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():
|
||||
try:
|
||||
update_expression = "SET a = if_not_exists(b, a.c)"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "a": {"S": "A"}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).validate()
|
||||
assert False, "No exception raised"
|
||||
except AttributeDoesNotExist:
|
||||
assert True
|
||||
|
||||
|
||||
def get_first_node_of_type(ast, node_type):
|
||||
return next(NodeDepthLeftTypeFetcher(node_type, ast))
|
||||
|
||||
|
||||
def get_set_action_value(ast):
|
||||
"""
|
||||
Helper that takes an AST and gets the first UpdateExpressionSetAction and retrieves the value of that action.
|
||||
This should only be called on validated expressions.
|
||||
Args:
|
||||
ast(Node):
|
||||
|
||||
Returns:
|
||||
DynamoType: The DynamoType object representing the Dynamo value.
|
||||
"""
|
||||
set_action = get_first_node_of_type(ast, UpdateExpressionSetAction)
|
||||
typed_value = set_action.children[1]
|
||||
assert isinstance(typed_value, DDBTypedValue)
|
||||
dynamo_value = typed_value.children[0]
|
||||
assert isinstance(dynamo_value, DynamoType)
|
||||
return dynamo_value
|
||||
|
||||
|
||||
def test_validation_of_if_not_exists_not_existing_value():
|
||||
update_expression = "SET a = if_not_exists(b, a)"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "a": {"S": "A"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).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():
|
||||
update_expression = "SET a = if_not_exists(b, a)"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "a": {"S": "A"}, "b": {"S": "B"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).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():
|
||||
update_expression = "SET a = if_not_exists(b, :val)"
|
||||
update_expression_values = {":val": {"N": "4"}}
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "b": {"N": "3"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=update_expression_values,
|
||||
item=item,
|
||||
).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():
|
||||
update_expression = "SET a = if_not_exists(b, :val)"
|
||||
update_expression_values = {":val": {"N": "4"}}
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=update_expression_values,
|
||||
item=item,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
assert dynamo_value == DynamoType({"N": "4"})
|
||||
|
||||
|
||||
def test_validation_of_sum_operation():
|
||||
update_expression = "SET a = a + b"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "a": {"N": "3"}, "b": {"N": "4"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
assert dynamo_value == DynamoType({"N": "7"})
|
||||
|
||||
|
||||
def test_validation_homogeneous_list_append_function():
|
||||
update_expression = "SET ri = list_append(ri, :vals)"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "ri": {"L": [{"S": "i1"}, {"S": "i2"}]}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":vals": {"L": [{"S": "i3"}, {"S": "i4"}]}},
|
||||
item=item,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
assert dynamo_value == DynamoType(
|
||||
{"L": [{"S": "i1"}, {"S": "i2"}, {"S": "i3"}, {"S": "i4"}]}
|
||||
)
|
||||
|
||||
|
||||
def test_validation_hetereogenous_list_append_function():
|
||||
update_expression = "SET ri = list_append(ri, :vals)"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "ri": {"L": [{"S": "i1"}, {"S": "i2"}]}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":vals": {"L": [{"N": "3"}]}},
|
||||
item=item,
|
||||
).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():
|
||||
"""
|
||||
Must error out:
|
||||
Invalid UpdateExpression: Incorrect operand type for operator or function;
|
||||
operator or function: list_append, operand type: S'
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
update_expression = "SET ri = list_append(ri, :vals)"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "ri": {"L": [{"S": "i1"}, {"S": "i2"}]}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":vals": {"S": "N"}},
|
||||
item=item,
|
||||
).validate()
|
||||
except IncorrectOperandType as e:
|
||||
assert e.operand_type == "S"
|
||||
assert e.operator_or_function == "list_append"
|
||||
|
||||
|
||||
def test_sum_with_incompatible_types():
|
||||
"""
|
||||
Must error out:
|
||||
Invalid UpdateExpression: Incorrect operand type for operator or function; operator or function: +, operand type: S'
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
update_expression = "SET ri = :val + :val2"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "ri": {"L": [{"S": "i1"}, {"S": "i2"}]}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":val": {"S": "N"}, ":val2": {"N": "3"}},
|
||||
item=item,
|
||||
).validate()
|
||||
except IncorrectOperandType as e:
|
||||
assert e.operand_type == "S"
|
||||
assert e.operator_or_function == "+"
|
||||
|
||||
|
||||
def test_validation_of_subraction_operation():
|
||||
update_expression = "SET ri = :val - :val2"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "1"}, "a": {"N": "3"}, "b": {"N": "4"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":val": {"N": "1"}, ":val2": {"N": "3"}},
|
||||
item=item,
|
||||
).validate()
|
||||
dynamo_value = get_set_action_value(validated_ast)
|
||||
assert dynamo_value == DynamoType({"N": "-2"})
|
||||
|
||||
|
||||
def test_cannot_index_into_a_string():
|
||||
"""
|
||||
Must error out:
|
||||
The document path provided in the update expression is invalid for update'
|
||||
"""
|
||||
try:
|
||||
update_expression = "set itemstr[1]=:Item"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "foo2"}, "itemstr": {"S": "somestring"}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values={":Item": {"S": "string_update"}},
|
||||
item=item,
|
||||
).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():
|
||||
"""If this step just passes we are happy enough"""
|
||||
update_expression = "set d=a"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "foo2"}, "a": {"N": "3"}},
|
||||
)
|
||||
validated_ast = UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).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():
|
||||
try:
|
||||
update_expression = "set d.e=a"
|
||||
update_expression_ast = UpdateExpressionParser.make(update_expression)
|
||||
item = Item(
|
||||
hash_key=DynamoType({"S": "id"}),
|
||||
hash_key_type="TYPE",
|
||||
range_key=None,
|
||||
range_key_type=None,
|
||||
attrs={"id": {"S": "foo2"}, "a": {"N": "3"}},
|
||||
)
|
||||
UpdateExpressionValidator(
|
||||
update_expression_ast,
|
||||
expression_attribute_names=None,
|
||||
expression_attribute_values=None,
|
||||
item=item,
|
||||
).validate()
|
||||
assert False, "Must raise exception"
|
||||
except InvalidUpdateExpressionInvalidDocumentPath:
|
||||
assert True
|
||||
Loading…
Add table
Add a link
Reference in a new issue