Adding dynamodb2 expression parser and fixing test cases
This commit is contained in:
parent
2275c53b3e
commit
1a2fc66f84
File diff suppressed because it is too large
Load Diff
617
moto/dynamodb2/condition.py
Normal file
617
moto/dynamodb2/condition.py
Normal file
@ -0,0 +1,617 @@
|
|||||||
|
import re
|
||||||
|
import json
|
||||||
|
import enum
|
||||||
|
from collections import deque
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
|
class Kind(enum.Enum):
|
||||||
|
"""Defines types of nodes in the syntax tree."""
|
||||||
|
|
||||||
|
# Condition nodes
|
||||||
|
# ---------------
|
||||||
|
OR = enum.auto()
|
||||||
|
AND = enum.auto()
|
||||||
|
NOT = enum.auto()
|
||||||
|
PARENTHESES = enum.auto()
|
||||||
|
FUNCTION = enum.auto()
|
||||||
|
BETWEEN = enum.auto()
|
||||||
|
IN = enum.auto()
|
||||||
|
COMPARISON = enum.auto()
|
||||||
|
|
||||||
|
# Operand nodes
|
||||||
|
# -------------
|
||||||
|
EXPRESSION_ATTRIBUTE_VALUE = enum.auto()
|
||||||
|
PATH = enum.auto()
|
||||||
|
|
||||||
|
# Literal nodes
|
||||||
|
# --------------
|
||||||
|
LITERAL = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Nonterminal(enum.Enum):
|
||||||
|
"""Defines nonterminals for defining productions."""
|
||||||
|
CONDITION = enum.auto()
|
||||||
|
OPERAND = enum.auto()
|
||||||
|
COMPARATOR = enum.auto()
|
||||||
|
FUNCTION_NAME = enum.auto()
|
||||||
|
IDENTIFIER = enum.auto()
|
||||||
|
AND = enum.auto()
|
||||||
|
OR = enum.auto()
|
||||||
|
NOT = enum.auto()
|
||||||
|
BETWEEN = enum.auto()
|
||||||
|
IN = enum.auto()
|
||||||
|
COMMA = enum.auto()
|
||||||
|
LEFT_PAREN = enum.auto()
|
||||||
|
RIGHT_PAREN = enum.auto()
|
||||||
|
WHITESPACE = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
Node = namedtuple('Node', ['nonterminal', 'kind', 'text', 'value', 'children'])
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionExpressionParser:
|
||||||
|
def __init__(self, condition_expression, expression_attribute_names,
|
||||||
|
expression_attribute_values):
|
||||||
|
self.condition_expression = condition_expression
|
||||||
|
self.expression_attribute_names = expression_attribute_names
|
||||||
|
self.expression_attribute_values = expression_attribute_values
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
"""Returns a syntax tree for the expression.
|
||||||
|
|
||||||
|
The tree, and all of the nodes in the tree are a tuple of
|
||||||
|
- kind: str
|
||||||
|
- children/value:
|
||||||
|
list of nodes for parent nodes
|
||||||
|
value for leaf nodes
|
||||||
|
|
||||||
|
Raises AssertionError if the condition expression is invalid
|
||||||
|
Raises KeyError if expression attribute names/values are invalid
|
||||||
|
|
||||||
|
Here are the types of nodes that can be returned.
|
||||||
|
The types of child nodes are denoted with a colon (:).
|
||||||
|
An arbitrary number of children is denoted with ...
|
||||||
|
|
||||||
|
Condition:
|
||||||
|
('OR', [lhs : Condition, rhs : Condition])
|
||||||
|
('AND', [lhs: Condition, rhs: Condition])
|
||||||
|
('NOT', [argument: Condition])
|
||||||
|
('PARENTHESES', [argument: Condition])
|
||||||
|
('FUNCTION', [('LITERAL', function_name: str), argument: Operand, ...])
|
||||||
|
('BETWEEN', [query: Operand, low: Operand, high: Operand])
|
||||||
|
('IN', [query: Operand, possible_value: Operand, ...])
|
||||||
|
('COMPARISON', [lhs: Operand, ('LITERAL', comparator: str), rhs: Operand])
|
||||||
|
|
||||||
|
Operand:
|
||||||
|
('EXPRESSION_ATTRIBUTE_VALUE', value: dict, e.g. {'S': 'foobar'})
|
||||||
|
('PATH', [('LITERAL', path_element: str), ...])
|
||||||
|
NOTE: Expression attribute names will be expanded
|
||||||
|
|
||||||
|
Literal:
|
||||||
|
('LITERAL', value: str)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.condition_expression:
|
||||||
|
return None
|
||||||
|
nodes = self._lex_condition_expression()
|
||||||
|
nodes = self._parse_paths(nodes)
|
||||||
|
self._print_debug(nodes)
|
||||||
|
nodes = self._apply_comparator(nodes)
|
||||||
|
self._print_debug(nodes)
|
||||||
|
nodes = self._apply_in(nodes)
|
||||||
|
self._print_debug(nodes)
|
||||||
|
nodes = self._apply_between(nodes)
|
||||||
|
self._print_debug(nodes)
|
||||||
|
nodes = self._apply_functions(nodes)
|
||||||
|
self._print_debug(nodes)
|
||||||
|
nodes = self._apply_parens_and_booleans(nodes)
|
||||||
|
self._print_debug(nodes)
|
||||||
|
node = nodes[0]
|
||||||
|
return self._make_node_tree(node)
|
||||||
|
|
||||||
|
def _lex_condition_expression(self):
|
||||||
|
nodes = deque()
|
||||||
|
remaining_expression = self.condition_expression
|
||||||
|
while remaining_expression:
|
||||||
|
node, remaining_expression = \
|
||||||
|
self._lex_one_node(remaining_expression)
|
||||||
|
if node.nonterminal == Nonterminal.WHITESPACE:
|
||||||
|
continue
|
||||||
|
nodes.append(node)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def _lex_one_node(self, remaining_expression):
|
||||||
|
|
||||||
|
attribute_regex = '(:|#)?[A-z0-9\-_]+'
|
||||||
|
patterns = [(
|
||||||
|
Nonterminal.WHITESPACE, re.compile('^ +')
|
||||||
|
), (
|
||||||
|
Nonterminal.COMPARATOR, re.compile(
|
||||||
|
'^('
|
||||||
|
'=|'
|
||||||
|
'<>|'
|
||||||
|
'<|'
|
||||||
|
'<=|'
|
||||||
|
'>|'
|
||||||
|
'>=)'),
|
||||||
|
), (
|
||||||
|
Nonterminal.OPERAND, re.compile(
|
||||||
|
'^' +
|
||||||
|
attribute_regex + '(\.' + attribute_regex + ')*')
|
||||||
|
), (
|
||||||
|
Nonterminal.COMMA, re.compile('^,')
|
||||||
|
), (
|
||||||
|
Nonterminal.LEFT_PAREN, re.compile('^\(')
|
||||||
|
), (
|
||||||
|
Nonterminal.RIGHT_PAREN, re.compile('^\)')
|
||||||
|
)]
|
||||||
|
|
||||||
|
for nonterminal, pattern in patterns:
|
||||||
|
match = pattern.match(remaining_expression)
|
||||||
|
if match:
|
||||||
|
match_text = match.group()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AssertionError("Cannot parse condition starting at: " +
|
||||||
|
remaining_expression)
|
||||||
|
|
||||||
|
value = match_text
|
||||||
|
node = Node(
|
||||||
|
nonterminal=nonterminal,
|
||||||
|
kind=Kind.LITERAL,
|
||||||
|
text=match_text,
|
||||||
|
value=match_text,
|
||||||
|
children=[])
|
||||||
|
|
||||||
|
remaining_expression = remaining_expression[len(match_text):]
|
||||||
|
|
||||||
|
return node, remaining_expression
|
||||||
|
|
||||||
|
def _parse_paths(self, nodes):
|
||||||
|
output = deque()
|
||||||
|
|
||||||
|
while nodes:
|
||||||
|
node = nodes.popleft()
|
||||||
|
|
||||||
|
if node.nonterminal == Nonterminal.OPERAND:
|
||||||
|
path = node.value.split('.')
|
||||||
|
children = [
|
||||||
|
self._parse_path_element(name)
|
||||||
|
for name in path]
|
||||||
|
if len(children) == 1:
|
||||||
|
child = children[0]
|
||||||
|
if child.nonterminal != Nonterminal.IDENTIFIER:
|
||||||
|
output.append(child)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
for child in children:
|
||||||
|
self._assert(
|
||||||
|
child.nonterminal == Nonterminal.IDENTIFIER,
|
||||||
|
"Cannot use %s in path" % child.text, [node])
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.OPERAND,
|
||||||
|
kind=Kind.PATH,
|
||||||
|
text=node.text,
|
||||||
|
value=None,
|
||||||
|
children=children))
|
||||||
|
else:
|
||||||
|
output.append(node)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _parse_path_element(self, name):
|
||||||
|
reserved = {
|
||||||
|
'AND': Nonterminal.AND,
|
||||||
|
'OR': Nonterminal.OR,
|
||||||
|
'IN': Nonterminal.IN,
|
||||||
|
'BETWEEN': Nonterminal.BETWEEN,
|
||||||
|
'NOT': Nonterminal.NOT,
|
||||||
|
}
|
||||||
|
|
||||||
|
functions = {
|
||||||
|
'attribute_exists',
|
||||||
|
'attribute_not_exists',
|
||||||
|
'attribute_type',
|
||||||
|
'begins_with',
|
||||||
|
'contains',
|
||||||
|
'size',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if name in reserved:
|
||||||
|
nonterminal = reserved[name]
|
||||||
|
return Node(
|
||||||
|
nonterminal=nonterminal,
|
||||||
|
kind=Kind.LITERAL,
|
||||||
|
text=name,
|
||||||
|
value=name,
|
||||||
|
children=[])
|
||||||
|
elif name in functions:
|
||||||
|
return Node(
|
||||||
|
nonterminal=Nonterminal.FUNCTION_NAME,
|
||||||
|
kind=Kind.LITERAL,
|
||||||
|
text=name,
|
||||||
|
value=name,
|
||||||
|
children=[])
|
||||||
|
elif name.startswith(':'):
|
||||||
|
return Node(
|
||||||
|
nonterminal=Nonterminal.OPERAND,
|
||||||
|
kind=Kind.EXPRESSION_ATTRIBUTE_VALUE,
|
||||||
|
text=name,
|
||||||
|
value=self._lookup_expression_attribute_value(name),
|
||||||
|
children=[])
|
||||||
|
elif name.startswith('#'):
|
||||||
|
return Node(
|
||||||
|
nonterminal=Nonterminal.IDENTIFIER,
|
||||||
|
kind=Kind.LITERAL,
|
||||||
|
text=name,
|
||||||
|
value=self._lookup_expression_attribute_name(name),
|
||||||
|
children=[])
|
||||||
|
else:
|
||||||
|
return Node(
|
||||||
|
nonterminal=Nonterminal.IDENTIFIER,
|
||||||
|
kind=Kind.LITERAL,
|
||||||
|
text=name,
|
||||||
|
value=name,
|
||||||
|
children=[])
|
||||||
|
|
||||||
|
def _lookup_expression_attribute_value(self, name):
|
||||||
|
return self.expression_attribute_values[name]
|
||||||
|
|
||||||
|
def _lookup_expression_attribute_name(self, name):
|
||||||
|
return self.expression_attribute_names[name]
|
||||||
|
|
||||||
|
# NOTE: The following constructions are ordered from high precedence to low precedence
|
||||||
|
# according to
|
||||||
|
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Precedence
|
||||||
|
#
|
||||||
|
# = <> < <= > >=
|
||||||
|
# IN
|
||||||
|
# BETWEEN
|
||||||
|
# attribute_exists attribute_not_exists begins_with contains
|
||||||
|
# Parentheses
|
||||||
|
# NOT
|
||||||
|
# AND
|
||||||
|
# OR
|
||||||
|
#
|
||||||
|
# The grammar is taken from
|
||||||
|
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Syntax
|
||||||
|
#
|
||||||
|
# condition-expression ::=
|
||||||
|
# operand comparator operand
|
||||||
|
# operand BETWEEN operand AND operand
|
||||||
|
# operand IN ( operand (',' operand (, ...) ))
|
||||||
|
# function
|
||||||
|
# condition AND condition
|
||||||
|
# condition OR condition
|
||||||
|
# NOT condition
|
||||||
|
# ( condition )
|
||||||
|
#
|
||||||
|
# comparator ::=
|
||||||
|
# =
|
||||||
|
# <>
|
||||||
|
# <
|
||||||
|
# <=
|
||||||
|
# >
|
||||||
|
# >=
|
||||||
|
#
|
||||||
|
# function ::=
|
||||||
|
# attribute_exists (path)
|
||||||
|
# attribute_not_exists (path)
|
||||||
|
# attribute_type (path, type)
|
||||||
|
# begins_with (path, substr)
|
||||||
|
# contains (path, operand)
|
||||||
|
# size (path)
|
||||||
|
|
||||||
|
def _matches(self, nodes, production):
|
||||||
|
"""Check if the nodes start with the given production.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
nodes: list of Node
|
||||||
|
production: list of str
|
||||||
|
The name of a Nonterminal, or '*' for anything
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(nodes) < len(production):
|
||||||
|
return False
|
||||||
|
for i in range(len(production)):
|
||||||
|
if production[i] == '*':
|
||||||
|
continue
|
||||||
|
expected = getattr(Nonterminal, production[i])
|
||||||
|
if nodes[i].nonterminal != expected:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _apply_comparator(self, nodes):
|
||||||
|
"""Apply condition := operand comparator operand."""
|
||||||
|
output = deque()
|
||||||
|
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['*', 'COMPARATOR']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['OPERAND', 'COMPARATOR', 'OPERAND']),
|
||||||
|
"Bad comparison", list(nodes)[:3])
|
||||||
|
lhs = nodes.popleft()
|
||||||
|
comparator = nodes.popleft()
|
||||||
|
rhs = nodes.popleft()
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.COMPARISON,
|
||||||
|
text=" ".join([
|
||||||
|
lhs.text,
|
||||||
|
comparator.text,
|
||||||
|
rhs.text]),
|
||||||
|
value=None,
|
||||||
|
children=[lhs, comparator, rhs]))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _apply_in(self, nodes):
|
||||||
|
"""Apply condition := operand IN ( operand , ... )."""
|
||||||
|
output = deque()
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['*', 'IN']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['OPERAND', 'IN', 'LEFT_PAREN']),
|
||||||
|
"Bad IN expression", list(nodes)[:3])
|
||||||
|
lhs = nodes.popleft()
|
||||||
|
in_node = nodes.popleft()
|
||||||
|
left_paren = nodes.popleft()
|
||||||
|
all_children = [lhs, in_node, left_paren]
|
||||||
|
rhs = []
|
||||||
|
while True:
|
||||||
|
if self._matches(nodes, ['OPERAND', 'COMMA']):
|
||||||
|
operand = nodes.popleft()
|
||||||
|
separator = nodes.popleft()
|
||||||
|
all_children += [operand, separator]
|
||||||
|
rhs.append(operand)
|
||||||
|
elif self._matches(nodes, ['OPERAND', 'RIGHT_PAREN']):
|
||||||
|
operand = nodes.popleft()
|
||||||
|
separator = nodes.popleft()
|
||||||
|
all_children += [operand, separator]
|
||||||
|
rhs.append(operand)
|
||||||
|
break # Close
|
||||||
|
else:
|
||||||
|
self._assert(
|
||||||
|
False,
|
||||||
|
"Bad IN expression starting at", nodes)
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.IN,
|
||||||
|
text=" ".join([t.text for t in all_children]),
|
||||||
|
value=None,
|
||||||
|
children=[lhs] + rhs))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _apply_between(self, nodes):
|
||||||
|
"""Apply condition := operand BETWEEN operand AND operand."""
|
||||||
|
output = deque()
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['*', 'BETWEEN']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['OPERAND', 'BETWEEN', 'OPERAND',
|
||||||
|
'AND', 'OPERAND']),
|
||||||
|
"Bad BETWEEN expression", list(nodes)[:5])
|
||||||
|
lhs = nodes.popleft()
|
||||||
|
between_node = nodes.popleft()
|
||||||
|
low = nodes.popleft()
|
||||||
|
and_node = nodes.popleft()
|
||||||
|
high = nodes.popleft()
|
||||||
|
all_children = [lhs, between_node, low, and_node, high]
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.BETWEEN,
|
||||||
|
text=" ".join([t.text for t in all_children]),
|
||||||
|
value=None,
|
||||||
|
children=[lhs, low, high]))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _apply_functions(self, nodes):
|
||||||
|
"""Apply condition := function_name (operand , ...)."""
|
||||||
|
output = deque()
|
||||||
|
expected_argument_kind_map = {
|
||||||
|
'attribute_exists': [{Kind.PATH}],
|
||||||
|
'attribute_not_exists': [{Kind.PATH}],
|
||||||
|
'attribute_type': [{Kind.PATH}, {Kind.EXPRESSION_ATTRIBUTE_VALUE}],
|
||||||
|
'begins_with': [{Kind.PATH}, {Kind.EXPRESSION_ATTRIBUTE_VALUE}],
|
||||||
|
'contains': [{Kind.PATH}, {Kind.PATH, Kind.EXPRESSION_ATTRIBUTE_VALUE}],
|
||||||
|
'size': [{Kind.PATH}],
|
||||||
|
}
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['FUNCTION_NAME']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['FUNCTION_NAME', 'LEFT_PAREN',
|
||||||
|
'OPERAND', '*']),
|
||||||
|
"Bad function expression at", list(nodes)[:4])
|
||||||
|
function_name = nodes.popleft()
|
||||||
|
left_paren = nodes.popleft()
|
||||||
|
all_children = [function_name, left_paren]
|
||||||
|
arguments = []
|
||||||
|
while True:
|
||||||
|
if self._matches(nodes, ['OPERAND', 'COMMA']):
|
||||||
|
operand = nodes.popleft()
|
||||||
|
separator = nodes.popleft()
|
||||||
|
all_children += [operand, separator]
|
||||||
|
arguments.append(operand)
|
||||||
|
elif self._matches(nodes, ['OPERAND', 'RIGHT_PAREN']):
|
||||||
|
operand = nodes.popleft()
|
||||||
|
separator = nodes.popleft()
|
||||||
|
all_children += [operand, separator]
|
||||||
|
arguments.append(operand)
|
||||||
|
break # Close paren
|
||||||
|
else:
|
||||||
|
self._assert(
|
||||||
|
False,
|
||||||
|
"Bad function expression", all_children + list(nodes)[:2])
|
||||||
|
expected_kinds = expected_argument_kind_map[function_name.value]
|
||||||
|
self._assert(
|
||||||
|
len(arguments) == len(expected_kinds),
|
||||||
|
"Wrong number of arguments in", all_children)
|
||||||
|
for i in range(len(expected_kinds)):
|
||||||
|
self._assert(
|
||||||
|
arguments[i].kind in expected_kinds[i],
|
||||||
|
"Wrong type for argument %d in" % i, all_children)
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.FUNCTION,
|
||||||
|
text=" ".join([t.text for t in all_children]),
|
||||||
|
value=None,
|
||||||
|
children=[function_name] + arguments))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _apply_parens_and_booleans(self, nodes, left_paren=None):
|
||||||
|
"""Apply condition := ( condition ) and booleans."""
|
||||||
|
output = deque()
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['LEFT_PAREN']):
|
||||||
|
parsed = self._apply_parens_and_booleans(nodes, left_paren=nodes.popleft())
|
||||||
|
self._assert(
|
||||||
|
len(parsed) >= 1,
|
||||||
|
"Failed to close parentheses at", nodes)
|
||||||
|
parens = parsed.popleft()
|
||||||
|
self._assert(
|
||||||
|
parens.kind == Kind.PARENTHESES,
|
||||||
|
"Failed to close parentheses at", nodes)
|
||||||
|
output.append(parens)
|
||||||
|
nodes = parsed
|
||||||
|
elif self._matches(nodes, ['RIGHT_PAREN']):
|
||||||
|
self._assert(
|
||||||
|
left_paren is not None,
|
||||||
|
"Unmatched ) at", nodes)
|
||||||
|
close_paren = nodes.popleft()
|
||||||
|
children = self._apply_booleans(output)
|
||||||
|
all_children = [left_paren, *children, close_paren]
|
||||||
|
return deque([
|
||||||
|
Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.PARENTHESES,
|
||||||
|
text=" ".join([t.text for t in all_children]),
|
||||||
|
value=None,
|
||||||
|
children=list(children),
|
||||||
|
), *nodes])
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
|
||||||
|
self._assert(
|
||||||
|
left_paren is None,
|
||||||
|
"Unmatched ( at", list(output))
|
||||||
|
return self._apply_booleans(output)
|
||||||
|
|
||||||
|
def _apply_booleans(self, nodes):
|
||||||
|
"""Apply and, or, and not constructions."""
|
||||||
|
nodes = self._apply_not(nodes)
|
||||||
|
nodes = self._apply_and(nodes)
|
||||||
|
nodes = self._apply_or(nodes)
|
||||||
|
# The expression should reduce to a single condition
|
||||||
|
self._assert(
|
||||||
|
len(nodes) == 1,
|
||||||
|
"Unexpected expression at", list(nodes)[1:])
|
||||||
|
self._assert(
|
||||||
|
nodes[0].nonterminal == Nonterminal.CONDITION,
|
||||||
|
"Incomplete condition", nodes)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def _apply_not(self, nodes):
|
||||||
|
"""Apply condition := NOT condition."""
|
||||||
|
output = deque()
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['NOT']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['NOT', 'CONDITION']),
|
||||||
|
"Bad NOT expression", list(nodes)[:2])
|
||||||
|
not_node = nodes.popleft()
|
||||||
|
child = nodes.popleft()
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.NOT,
|
||||||
|
text=" ".join([not_node['text'], value['text']]),
|
||||||
|
value=None,
|
||||||
|
children=[child]))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _apply_and(self, nodes):
|
||||||
|
"""Apply condition := condition AND condition."""
|
||||||
|
output = deque()
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['*', 'AND']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['CONDITION', 'AND', 'CONDITION']),
|
||||||
|
"Bad AND expression", list(nodes)[:3])
|
||||||
|
lhs = nodes.popleft()
|
||||||
|
and_node = nodes.popleft()
|
||||||
|
rhs = nodes.popleft()
|
||||||
|
all_children = [lhs, and_node, rhs]
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.AND,
|
||||||
|
text=" ".join([t.text for t in all_children]),
|
||||||
|
value=None,
|
||||||
|
children=[lhs, rhs]))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _apply_or(self, nodes):
|
||||||
|
"""Apply condition := condition OR condition."""
|
||||||
|
output = deque()
|
||||||
|
while nodes:
|
||||||
|
if self._matches(nodes, ['*', 'OR']):
|
||||||
|
self._assert(
|
||||||
|
self._matches(nodes, ['CONDITION', 'OR', 'CONDITION']),
|
||||||
|
"Bad OR expression", list(nodes)[:3])
|
||||||
|
lhs = nodes.popleft()
|
||||||
|
or_node = nodes.popleft()
|
||||||
|
rhs = nodes.popleft()
|
||||||
|
all_children = [lhs, or_node, rhs]
|
||||||
|
output.append(Node(
|
||||||
|
nonterminal=Nonterminal.CONDITION,
|
||||||
|
kind=Kind.OR,
|
||||||
|
text=" ".join([t.text for t in all_children]),
|
||||||
|
value=None,
|
||||||
|
children=[lhs, rhs]))
|
||||||
|
else:
|
||||||
|
output.append(nodes.popleft())
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def _make_node_tree(self, node):
|
||||||
|
if len(node.children) > 0:
|
||||||
|
return (
|
||||||
|
node.kind.name,
|
||||||
|
[
|
||||||
|
self._make_node_tree(child)
|
||||||
|
for child in node.children
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
return (node.kind.name, node.value)
|
||||||
|
|
||||||
|
def _print_debug(self, nodes):
|
||||||
|
print('ROOT')
|
||||||
|
for node in nodes:
|
||||||
|
self._print_node_recursive(node, depth=1)
|
||||||
|
|
||||||
|
def _print_node_recursive(self, node, depth=0):
|
||||||
|
if len(node.children) > 0:
|
||||||
|
print(' ' * depth, node.nonterminal, node.kind)
|
||||||
|
for child in node.children:
|
||||||
|
self._print_node_recursive(child, depth=depth + 1)
|
||||||
|
else:
|
||||||
|
print(' ' * depth, node.nonterminal, node.kind, node.value)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _assert(self, condition, message, nodes):
|
||||||
|
if not condition:
|
||||||
|
raise AssertionError(message + " " + " ".join([t.text for t in nodes]))
|
@ -68,10 +68,34 @@ class DynamoType(object):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return float(self.value)
|
return float(self.value)
|
||||||
elif self.is_set():
|
elif self.is_set():
|
||||||
return set(self.value)
|
sub_type = self.type[0]
|
||||||
|
return set([DynamoType({sub_type: v}).cast_value
|
||||||
|
for v in self.value])
|
||||||
|
elif self.is_list():
|
||||||
|
return [DynamoType(v).cast_value for v in self.value]
|
||||||
|
elif self.is_map():
|
||||||
|
return dict([
|
||||||
|
(k, DynamoType(v).cast_value)
|
||||||
|
for k, v in self.value.items()])
|
||||||
else:
|
else:
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
def child_attr(self, key):
|
||||||
|
"""
|
||||||
|
Get Map or List children by key. str for Map, int for List.
|
||||||
|
|
||||||
|
Returns DynamoType or None.
|
||||||
|
"""
|
||||||
|
if isinstance(key, str) and self.is_map() and key in self.value:
|
||||||
|
return DynamoType(self.value[key])
|
||||||
|
|
||||||
|
if isinstance(key, int) and self.is_list():
|
||||||
|
idx = key
|
||||||
|
if idx >= 0 and idx < len(self.value):
|
||||||
|
return DynamoType(self.value[idx])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def to_json(self):
|
def to_json(self):
|
||||||
return {self.type: self.value}
|
return {self.type: self.value}
|
||||||
|
|
||||||
@ -89,6 +113,12 @@ class DynamoType(object):
|
|||||||
def is_set(self):
|
def is_set(self):
|
||||||
return self.type == 'SS' or self.type == 'NS' or self.type == 'BS'
|
return self.type == 'SS' or self.type == 'NS' or self.type == 'BS'
|
||||||
|
|
||||||
|
def is_list(self):
|
||||||
|
return self.type == 'L'
|
||||||
|
|
||||||
|
def is_map(self):
|
||||||
|
return self.type == 'M'
|
||||||
|
|
||||||
def same_type(self, other):
|
def same_type(self, other):
|
||||||
return self.type == other.type
|
return self.type == other.type
|
||||||
|
|
||||||
@ -954,10 +984,7 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
range_values = [DynamoType(range_value)
|
range_values = [DynamoType(range_value)
|
||||||
for range_value in range_value_dicts]
|
for range_value in range_value_dicts]
|
||||||
|
|
||||||
if filter_expression is not None:
|
|
||||||
filter_expression = get_filter_expression(filter_expression, expr_names, expr_values)
|
filter_expression = get_filter_expression(filter_expression, expr_names, expr_values)
|
||||||
else:
|
|
||||||
filter_expression = Op(None, None) # Will always eval to true
|
|
||||||
|
|
||||||
return table.query(hash_key, range_comparison, range_values, limit,
|
return table.query(hash_key, range_comparison, range_values, limit,
|
||||||
exclusive_start_key, scan_index_forward, projection_expression, index_name, filter_expression, **filter_kwargs)
|
exclusive_start_key, scan_index_forward, projection_expression, index_name, filter_expression, **filter_kwargs)
|
||||||
@ -972,10 +999,8 @@ class DynamoDBBackend(BaseBackend):
|
|||||||
dynamo_types = [DynamoType(value) for value in comparison_values]
|
dynamo_types = [DynamoType(value) for value in comparison_values]
|
||||||
scan_filters[key] = (comparison_operator, dynamo_types)
|
scan_filters[key] = (comparison_operator, dynamo_types)
|
||||||
|
|
||||||
if filter_expression is not None:
|
|
||||||
filter_expression = get_filter_expression(filter_expression, expr_names, expr_values)
|
filter_expression = get_filter_expression(filter_expression, expr_names, expr_values)
|
||||||
else:
|
|
||||||
filter_expression = Op(None, None) # Will always eval to true
|
|
||||||
|
|
||||||
return table.scan(scan_filters, limit, exclusive_start_key, filter_expression, index_name)
|
return table.scan(scan_filters, limit, exclusive_start_key, filter_expression, index_name)
|
||||||
|
|
||||||
|
@ -676,44 +676,47 @@ def test_filter_expression():
|
|||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
# NOT test 2
|
# NOT test 2
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('NOT (Id = :v0)', {}, {':v0': {'N': 8}})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('NOT (Id = :v0)', {}, {':v0': {'N': '8'}})
|
||||||
filter_expr.expr(row1).should.be(False) # Id = 8 so should be false
|
filter_expr.expr(row1).should.be(False) # Id = 8 so should be false
|
||||||
|
|
||||||
# AND test
|
# AND test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id > :v0 AND Subs < :v1', {}, {':v0': {'N': 5}, ':v1': {'N': 7}})
|
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(row1).should.be(True)
|
||||||
filter_expr.expr(row2).should.be(False)
|
filter_expr.expr(row2).should.be(False)
|
||||||
|
|
||||||
# OR test
|
# OR test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 OR Id=:v1', {}, {':v0': {'N': 5}, ':v1': {'N': 8}})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 OR Id=:v1', {}, {':v0': {'N': '5'}, ':v1': {'N': '8'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
# BETWEEN test
|
# BETWEEN test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id BETWEEN :v0 AND :v1', {}, {':v0': {'N': 5}, ':v1': {'N': 10}})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id BETWEEN :v0 AND :v1', {}, {':v0': {'N': '5'}, ':v1': {'N': '10'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
# PAREN test
|
# PAREN test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 AND (Subs = :v0 OR Subs = :v1)', {}, {':v0': {'N': 8}, ':v1': {'N': 5}})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id = :v0 AND (Subs = :v0 OR Subs = :v1)', {}, {':v0': {'N': '8'}, ':v1': {'N': '5'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
# IN test
|
# IN test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id IN :v0', {}, {':v0': {'NS': [7, 8, 9]}})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('Id IN (:v0, :v1, :v2)', {}, {
|
||||||
|
':v0': {'N': '7'},
|
||||||
|
':v1': {'N': '8'},
|
||||||
|
':v2': {'N': '9'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
# attribute function tests (with extra spaces)
|
# attribute function tests (with extra spaces)
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_exists(Id) AND attribute_not_exists (User)', {}, {})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_exists(Id) AND attribute_not_exists (User)', {}, {})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_type(Id, N)', {}, {})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('attribute_type(Id, :v0)', {}, {':v0': {'S': 'N'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
|
|
||||||
# beginswith function test
|
# beginswith function test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('begins_with(Desc, Some)', {}, {})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('begins_with(Desc, :v0)', {}, {':v0': {'S': 'Some'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
filter_expr.expr(row2).should.be(False)
|
filter_expr.expr(row2).should.be(False)
|
||||||
|
|
||||||
# contains function test
|
# contains function test
|
||||||
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('contains(KV, test1)', {}, {})
|
filter_expr = moto.dynamodb2.comparisons.get_filter_expression('contains(KV, :v0)', {}, {':v0': {'S': 'test1'}})
|
||||||
filter_expr.expr(row1).should.be(True)
|
filter_expr.expr(row1).should.be(True)
|
||||||
filter_expr.expr(row2).should.be(False)
|
filter_expr.expr(row2).should.be(False)
|
||||||
|
|
||||||
@ -754,14 +757,26 @@ def test_query_filter():
|
|||||||
TableName='test1',
|
TableName='test1',
|
||||||
Item={
|
Item={
|
||||||
'client': {'S': 'client1'},
|
'client': {'S': 'client1'},
|
||||||
'app': {'S': 'app1'}
|
'app': {'S': 'app1'},
|
||||||
|
'nested': {'M': {
|
||||||
|
'version': {'S': 'version1'},
|
||||||
|
'contents': {'L': [
|
||||||
|
{'S': 'value1'}, {'S': 'value2'},
|
||||||
|
]},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
client.put_item(
|
client.put_item(
|
||||||
TableName='test1',
|
TableName='test1',
|
||||||
Item={
|
Item={
|
||||||
'client': {'S': 'client1'},
|
'client': {'S': 'client1'},
|
||||||
'app': {'S': 'app2'}
|
'app': {'S': 'app2'},
|
||||||
|
'nested': {'M': {
|
||||||
|
'version': {'S': 'version2'},
|
||||||
|
'contents': {'L': [
|
||||||
|
{'S': 'value1'}, {'S': 'value2'},
|
||||||
|
]},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -783,6 +798,18 @@ def test_query_filter():
|
|||||||
)
|
)
|
||||||
assert response['Count'] == 2
|
assert response['Count'] == 2
|
||||||
|
|
||||||
|
response = table.query(
|
||||||
|
KeyConditionExpression=Key('client').eq('client1'),
|
||||||
|
FilterExpression=Attr('nested.version').contains('version')
|
||||||
|
)
|
||||||
|
assert response['Count'] == 2
|
||||||
|
|
||||||
|
response = table.query(
|
||||||
|
KeyConditionExpression=Key('client').eq('client1'),
|
||||||
|
FilterExpression=Attr('nested.contents[0]').eq('value1')
|
||||||
|
)
|
||||||
|
assert response['Count'] == 2
|
||||||
|
|
||||||
|
|
||||||
@mock_dynamodb2
|
@mock_dynamodb2
|
||||||
def test_scan_filter():
|
def test_scan_filter():
|
||||||
|
Loading…
Reference in New Issue
Block a user