443 lines
14 KiB
Python
443 lines
14 KiB
Python
# type: ignore
|
|
import abc
|
|
from abc import abstractmethod
|
|
from collections import deque
|
|
|
|
from moto.dynamodb.models import DynamoType
|
|
|
|
from ..exceptions import DuplicateUpdateExpression, TooManyAddClauses
|
|
|
|
|
|
class Node(metaclass=abc.ABCMeta):
|
|
def __init__(self, children=None):
|
|
self.type = self.__class__.__name__
|
|
assert children is None or isinstance(children, list)
|
|
self.children = children
|
|
self.parent = None
|
|
|
|
if isinstance(children, list):
|
|
for child in children:
|
|
if isinstance(child, Node):
|
|
child.set_parent(self)
|
|
|
|
def set_parent(self, parent_node):
|
|
self.parent = parent_node
|
|
|
|
def validate(self) -> None:
|
|
if self.type == "UpdateExpression":
|
|
nr_of_clauses = len(self.find_clauses([UpdateExpressionAddClause]))
|
|
if nr_of_clauses > 1:
|
|
raise TooManyAddClauses()
|
|
set_actions = self.find_clauses([UpdateExpressionSetAction])
|
|
# set_attributes = ["attr", "map.attr", attr.list[2], ..]
|
|
set_attributes = [s.children[0].to_str() for s in set_actions]
|
|
# We currently only check for duplicates
|
|
# We should also check for partial duplicates, i.e. [attr, attr.sub] is also invalid
|
|
if len(set_attributes) != len(set(set_attributes)):
|
|
raise DuplicateUpdateExpression(set_attributes)
|
|
|
|
def normalize(self):
|
|
"""
|
|
Flatten the Add-/Delete-/Remove-/Set-Action children within this Node
|
|
"""
|
|
if self.type == "UpdateExpression":
|
|
# We can have multiple REMOVE attr[idx] expressions, such as attr[i] and attr[i+2]
|
|
# If we remove attr[i] first, attr[i+2] suddenly refers to a different item
|
|
# So we sort them in reverse order - we can remove attr[i+2] first, attr[i] still refers to the same item
|
|
|
|
# Behaviour that is unknown, for now:
|
|
# What happens if we SET and REMOVE on the same list - what takes precedence?
|
|
# We're assuming this is executed in original order
|
|
|
|
remove_actions = []
|
|
sorted_actions = []
|
|
possible_clauses = [
|
|
UpdateExpressionAddAction,
|
|
UpdateExpressionDeleteAction,
|
|
UpdateExpressionRemoveAction,
|
|
UpdateExpressionSetAction,
|
|
]
|
|
for action in self.find_clauses(possible_clauses):
|
|
if isinstance(action, UpdateExpressionRemoveAction):
|
|
# Keep these separate for now
|
|
remove_actions.append(action)
|
|
else:
|
|
if len(remove_actions) > 0:
|
|
# Remove-actions were found earlier
|
|
# Now that we have other action-types, that means we've found all possible Remove-actions
|
|
# Sort them appropriately
|
|
sorted_actions.extend(sorted(remove_actions, reverse=True))
|
|
remove_actions.clear()
|
|
# Add other actions by insertion order
|
|
sorted_actions.append(action)
|
|
# Remove actions were found last
|
|
if len(remove_actions) > 0:
|
|
sorted_actions.extend(sorted(remove_actions, reverse=True))
|
|
|
|
self.children = sorted_actions
|
|
|
|
def find_clauses(self, clause_types):
|
|
clauses = []
|
|
for child in self.children or []:
|
|
if type(child) in clause_types:
|
|
clauses.append(child)
|
|
elif isinstance(child, Expression):
|
|
clauses.extend(child.find_clauses(clause_types))
|
|
return clauses
|
|
|
|
|
|
class LeafNode(Node):
|
|
"""A LeafNode is a Node where none of the children are Nodes themselves."""
|
|
|
|
def __init__(self, children=None):
|
|
super().__init__(children)
|
|
|
|
|
|
class Expression(Node, metaclass=abc.ABCMeta):
|
|
"""
|
|
Abstract Syntax Tree representing the expression
|
|
|
|
For the Grammar start here and jump down into the classes at the righ-hand side to look further. Nodes marked with
|
|
a star are abstract and won't appear in the final AST.
|
|
|
|
Expression* => UpdateExpression
|
|
Expression* => ConditionExpression
|
|
"""
|
|
|
|
|
|
class UpdateExpression(Expression):
|
|
"""
|
|
UpdateExpression => UpdateExpressionClause*
|
|
UpdateExpression => UpdateExpressionClause* UpdateExpression
|
|
"""
|
|
|
|
|
|
class UpdateExpressionClause(UpdateExpression, metaclass=abc.ABCMeta):
|
|
"""
|
|
UpdateExpressionClause* => UpdateExpressionSetClause
|
|
UpdateExpressionClause* => UpdateExpressionRemoveClause
|
|
UpdateExpressionClause* => UpdateExpressionAddClause
|
|
UpdateExpressionClause* => UpdateExpressionDeleteClause
|
|
"""
|
|
|
|
|
|
class UpdateExpressionSetClause(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionSetClause => SET SetActions
|
|
"""
|
|
|
|
|
|
class UpdateExpressionSetActions(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionSetClause => SET SetActions
|
|
|
|
SetActions => SetAction
|
|
SetActions => SetAction , SetActions
|
|
|
|
"""
|
|
|
|
|
|
class UpdateExpressionSetAction(UpdateExpressionClause):
|
|
"""
|
|
SetAction => Path = Value
|
|
"""
|
|
|
|
|
|
class UpdateExpressionRemoveActions(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionSetClause => REMOVE RemoveActions
|
|
|
|
RemoveActions => RemoveAction
|
|
RemoveActions => RemoveAction , RemoveActions
|
|
"""
|
|
|
|
|
|
class UpdateExpressionRemoveAction(UpdateExpressionClause):
|
|
"""
|
|
RemoveAction => Path
|
|
"""
|
|
|
|
def _get_value(self):
|
|
expression_path = self.children[0]
|
|
expression_selector = expression_path.children[-1]
|
|
return expression_selector.children[0]
|
|
|
|
def __lt__(self, other):
|
|
self_value = self._get_value()
|
|
|
|
return self_value < other._get_value()
|
|
|
|
|
|
class UpdateExpressionAddActions(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionAddClause => ADD RemoveActions
|
|
|
|
AddActions => AddAction
|
|
AddActions => AddAction , AddActions
|
|
"""
|
|
|
|
|
|
class UpdateExpressionAddAction(UpdateExpressionClause):
|
|
"""
|
|
AddAction => Path Value
|
|
"""
|
|
|
|
|
|
class UpdateExpressionDeleteActions(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionDeleteClause => DELETE RemoveActions
|
|
|
|
DeleteActions => DeleteAction
|
|
DeleteActions => DeleteAction , DeleteActions
|
|
"""
|
|
|
|
|
|
class UpdateExpressionDeleteAction(UpdateExpressionClause):
|
|
"""
|
|
DeleteAction => Path Value
|
|
"""
|
|
|
|
|
|
class UpdateExpressionPath(UpdateExpressionClause):
|
|
def to_str(self):
|
|
return "".join(x.to_str() for x in self.children)
|
|
|
|
|
|
class UpdateExpressionValue(UpdateExpressionClause):
|
|
"""
|
|
Value => Operand
|
|
Value => Operand + Value
|
|
Value => Operand - Value
|
|
"""
|
|
|
|
|
|
class UpdateExpressionGroupedValue(UpdateExpressionClause):
|
|
"""
|
|
GroupedValue => ( Value )
|
|
"""
|
|
|
|
|
|
class UpdateExpressionRemoveClause(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionRemoveClause => REMOVE RemoveActions
|
|
"""
|
|
|
|
|
|
class UpdateExpressionAddClause(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionAddClause => ADD AddActions
|
|
"""
|
|
|
|
|
|
class UpdateExpressionDeleteClause(UpdateExpressionClause):
|
|
"""
|
|
UpdateExpressionDeleteClause => DELETE DeleteActions
|
|
"""
|
|
|
|
|
|
class ExpressionPathDescender(Node):
|
|
"""Node identifying descender into nested structure (.) in expression"""
|
|
|
|
def to_str(self):
|
|
return "."
|
|
|
|
|
|
class ExpressionSelector(LeafNode):
|
|
"""Node identifying selector [selection_index] in expresion"""
|
|
|
|
def __init__(self, selection_index):
|
|
try:
|
|
super().__init__(children=[int(selection_index)])
|
|
except ValueError:
|
|
assert (
|
|
False
|
|
), "Expression selector must be an int, this is a bug in the moto library."
|
|
|
|
def get_index(self):
|
|
return self.children[0]
|
|
|
|
def to_str(self):
|
|
return f"[{self.get_index()}]"
|
|
|
|
|
|
class ExpressionAttribute(LeafNode):
|
|
"""An attribute identifier as used in the DDB item"""
|
|
|
|
def __init__(self, attribute):
|
|
super().__init__(children=[attribute])
|
|
|
|
def get_attribute_name(self):
|
|
return self.children[0]
|
|
|
|
def to_str(self):
|
|
return self.get_attribute_name()
|
|
|
|
|
|
class ExpressionAttributeName(LeafNode):
|
|
"""An ExpressionAttributeName is an alias for an attribute identifier"""
|
|
|
|
def __init__(self, attribute_name):
|
|
super().__init__(children=[attribute_name])
|
|
|
|
def get_attribute_name_placeholder(self):
|
|
return self.children[0]
|
|
|
|
def to_str(self):
|
|
return self.get_attribute_name_placeholder()
|
|
|
|
|
|
class ExpressionAttributeValue(LeafNode):
|
|
"""An ExpressionAttributeValue is an alias for an value"""
|
|
|
|
def __init__(self, value):
|
|
super().__init__(children=[value])
|
|
|
|
def get_value_name(self):
|
|
return self.children[0]
|
|
|
|
|
|
class ExpressionValueOperator(LeafNode):
|
|
"""An ExpressionValueOperator is an operation that works on 2 values"""
|
|
|
|
def __init__(self, value):
|
|
super().__init__(children=[value])
|
|
|
|
def get_operator(self):
|
|
return self.children[0]
|
|
|
|
|
|
class UpdateExpressionFunction(Node):
|
|
"""
|
|
A Node representing a function of an Update Expression. The first child is the function name the others are the
|
|
arguments.
|
|
"""
|
|
|
|
def get_function_name(self):
|
|
return self.children[0]
|
|
|
|
def get_nth_argument(self, n=1):
|
|
"""Return nth element where n is a 1-based index."""
|
|
assert n >= 1
|
|
return self.children[n]
|
|
|
|
|
|
class DDBTypedValue(Node):
|
|
"""
|
|
A node representing a DDBTyped value. This can be any structure as supported by DyanmoDB. The node only has 1 child
|
|
which is the value of type `DynamoType`.
|
|
"""
|
|
|
|
def __init__(self, value):
|
|
assert isinstance(value, DynamoType), "DDBTypedValue must be of DynamoType"
|
|
super().__init__(children=[value])
|
|
|
|
def get_value(self):
|
|
return self.children[0]
|
|
|
|
|
|
class NoneExistingPath(LeafNode):
|
|
"""A placeholder for Paths that did not exist in the Item."""
|
|
|
|
def __init__(self, creatable=False):
|
|
super().__init__(children=[creatable])
|
|
|
|
def is_creatable(self):
|
|
"""Can this path be created if need be. For example path creating element in a dictionary or creating a new
|
|
attribute under root level of an item."""
|
|
return self.children[0]
|
|
|
|
|
|
class DepthFirstTraverser(object):
|
|
"""
|
|
Helper class that allows depth first traversal and to implement custom processing for certain AST nodes. The
|
|
processor of a node must return the new resulting node. This node will be placed in the tree. Processing of a
|
|
node using this traverser should therefore only transform child nodes. The returned node will get the same parent
|
|
as the node before processing had.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def _processing_map(self):
|
|
"""
|
|
A map providing a processing function per node class type to a function that takes in a Node object and
|
|
processes it. A Node can only be processed by a single function and they are considered in order. Therefore if
|
|
multiple classes from a single class hierarchy strain are used the more specific classes have to be put before
|
|
the less specific ones. That requires overriding `nodes_to_be_processed`. If no multiple classes form a single
|
|
class hierarchy strain are used the default implementation of `nodes_to_be_processed` should be OK.
|
|
Returns:
|
|
dict: Mapping a Node Class to a processing function.
|
|
"""
|
|
pass
|
|
|
|
def nodes_to_be_processed(self):
|
|
"""Cached accessor for getting Node types that need to be processed."""
|
|
return tuple(k for k in self._processing_map().keys())
|
|
|
|
def process(self, node):
|
|
"""Process a Node"""
|
|
for class_key, processor in self._processing_map().items():
|
|
if isinstance(node, class_key):
|
|
return processor(node)
|
|
|
|
def pre_processing_of_child(self, parent_node, child_id):
|
|
"""Hook that is called pre-processing of the child at position `child_id`"""
|
|
pass
|
|
|
|
def traverse_node_recursively(self, node, child_id=-1):
|
|
"""
|
|
Traverse nodes depth first processing nodes bottom up (if root node is considered the top).
|
|
|
|
Args:
|
|
node(Node): The node which is the last node to be processed but which allows to identify all the
|
|
work (which is in the children)
|
|
child_id(int): The index in the list of children from the parent that this node corresponds to
|
|
|
|
Returns:
|
|
Node: The node of the new processed AST
|
|
"""
|
|
if isinstance(node, Node):
|
|
parent_node = node.parent
|
|
if node.children is not None:
|
|
for i, child_node in enumerate(node.children):
|
|
self.pre_processing_of_child(node, i)
|
|
self.traverse_node_recursively(child_node, i)
|
|
# noinspection PyTypeChecker
|
|
if isinstance(node, self.nodes_to_be_processed()):
|
|
node = self.process(node)
|
|
node.parent = parent_node
|
|
parent_node.children[child_id] = node
|
|
return node
|
|
|
|
def traverse(self, node):
|
|
return self.traverse_node_recursively(node)
|
|
|
|
|
|
class NodeDepthLeftTypeFetcher(object):
|
|
"""Helper class to fetch a node of a specific type. Depth left-first traversal"""
|
|
|
|
def __init__(self, node_type, root_node):
|
|
assert issubclass(node_type, Node)
|
|
self.node_type = node_type
|
|
self.root_node = root_node
|
|
self.queue = deque()
|
|
self.add_nodes_left_to_right_depth_first(self.root_node)
|
|
|
|
def add_nodes_left_to_right_depth_first(self, node):
|
|
if isinstance(node, Node) and node.children is not None:
|
|
for child_node in node.children:
|
|
self.add_nodes_left_to_right_depth_first(child_node)
|
|
self.queue.append(child_node)
|
|
self.queue.append(node)
|
|
|
|
def __iter__(self):
|
|
return self
|
|
|
|
def next(self):
|
|
return self.__next__()
|
|
|
|
def __next__(self):
|
|
while len(self.queue) > 0:
|
|
candidate = self.queue.popleft()
|
|
if isinstance(candidate, self.node_type):
|
|
return candidate
|
|
raise StopIteration
|