Better DDB expressions support2: ExpressionTree

Part of structured approach for UpdateExpressions:
 1) Expression gets parsed into a tokenlist (tokenized)
 2) Tokenlist get transformed to expression tree (AST) -> This commit
 3) The AST gets validated (full semantic correctness)
 4) AST gets processed to perform the update

This commit uses the tokenlist to build an expression tree. This tree is not
yet used. Still it allows to raise additional Validation Exceptions which
previously were missed silently therefore it allows tests to catch these type of
ValidationException. For that reason DDB UpdateExpressions will be parsed
already. It also makes sure we won't break existing tests.

One of the existing tests had to be changed in order to still pass:
 - test_dynamodb_table_with_range_key.test_update_item_with_expression

This test passed in a numeric literal which is not supported by DynamoDB
and with the current tokenization it would get the same error as in AWS
DynamoDB.
This commit is contained in:
pvbouwel 2020-04-11 21:17:16 +01:00
commit 9ed613e197
10 changed files with 2317 additions and 13 deletions

View file

@ -14,10 +14,11 @@ from moto.core import BaseBackend, BaseModel
from moto.core.utils import unix_time
from moto.core.exceptions import JsonRESTError
from moto.dynamodb2.comparisons import get_filter_expression
from moto.dynamodb2.comparisons import get_expected
from moto.dynamodb2.exceptions import InvalidIndexNameError, ItemSizeTooLarge
from moto.dynamodb2.comparisons import get_expected, get_comparison_func
from moto.dynamodb2.exceptions import InvalidIndexNameError, ItemSizeTooLarge, InvalidUpdateExpression
from moto.dynamodb2.models.utilities import bytesize, attribute_is_list
from moto.dynamodb2.models.dynamo_type import DynamoType
from moto.dynamodb2.parsing.expressions import UpdateExpressionParser
class DynamoJsonEncoder(json.JSONEncoder):
@ -1197,6 +1198,13 @@ class DynamoDBBackend(BaseBackend):
):
table = self.get_table(table_name)
# Support spaces between operators in an update expression
# E.g. `a = b + c` -> `a=b+c`
if update_expression:
# Parse expression to get validation errors
UpdateExpressionParser.make(update_expression)
update_expression = re.sub(r"\s*([=\+-])\s*", "\\1", update_expression)
if all([table.hash_key_attr in key, table.range_key_attr in key]):
# Covers cases where table has hash and range keys, ``key`` param
# will be a dict