support a bit more of the dynamoDB ConditionExpression syntax
This commit is contained in:
parent
1095b7d94b
commit
f035b9613d
@ -31,6 +31,68 @@ def get_empty_str_error():
|
|||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def condition_expression_to_expected(condition_expression, expression_attribute_names, expression_attribute_values):
|
||||||
|
"""
|
||||||
|
Limited condition expression syntax parsing.
|
||||||
|
Supports Global Negation ex: NOT(inner expressions).
|
||||||
|
Supports simple AND conditions ex: cond_a AND cond_b and cond_c.
|
||||||
|
Atomic expressions supported are attribute_exists(key), attribute_not_exists(key) and #key = :value.
|
||||||
|
"""
|
||||||
|
expected = {}
|
||||||
|
if condition_expression and 'OR' not in condition_expression:
|
||||||
|
reverse_re = re.compile('^NOT\s*\((.*)\)$')
|
||||||
|
reverse_m = reverse_re.match(condition_expression.strip())
|
||||||
|
|
||||||
|
reverse = False
|
||||||
|
if reverse_m:
|
||||||
|
reverse = True
|
||||||
|
condition_expression = reverse_m.group(1)
|
||||||
|
|
||||||
|
cond_items = [c.strip() for c in condition_expression.split('AND')]
|
||||||
|
if cond_items:
|
||||||
|
overwrite = False
|
||||||
|
exists_re = re.compile('^attribute_exists\s*\((.*)\)$')
|
||||||
|
not_exists_re = re.compile(
|
||||||
|
'^attribute_not_exists\s*\((.*)\)$')
|
||||||
|
equals_re= re.compile('^(#?\w+)\s*=\s*(\:?\w+)')
|
||||||
|
|
||||||
|
for cond in cond_items:
|
||||||
|
exists_m = exists_re.match(cond)
|
||||||
|
not_exists_m = not_exists_re.match(cond)
|
||||||
|
equals_m = equals_re.match(cond)
|
||||||
|
|
||||||
|
if exists_m:
|
||||||
|
attribute_name = expression_attribute_names_lookup(exists_m.group(1), expression_attribute_names)
|
||||||
|
expected[attribute_name] = {'Exists': True if not reverse else False}
|
||||||
|
elif not_exists_m:
|
||||||
|
attribute_name = expression_attribute_names_lookup(not_exists_m.group(1), expression_attribute_names)
|
||||||
|
expected[attribute_name] = {'Exists': False if not reverse else True}
|
||||||
|
elif equals_m:
|
||||||
|
attribute_name = expression_attribute_names_lookup(equals_m.group(1), expression_attribute_names)
|
||||||
|
attribute_value = expression_attribute_values_lookup(equals_m.group(2), expression_attribute_values)
|
||||||
|
expected[attribute_name] = {
|
||||||
|
'AttributeValueList': [attribute_value],
|
||||||
|
'ComparisonOperator': 'EQ' if not reverse else 'NEQ'}
|
||||||
|
|
||||||
|
return expected
|
||||||
|
|
||||||
|
|
||||||
|
def expression_attribute_names_lookup(attribute_name, expression_attribute_names):
|
||||||
|
if attribute_name.startswith('#') and attribute_name in expression_attribute_names:
|
||||||
|
return expression_attribute_names[attribute_name]
|
||||||
|
else:
|
||||||
|
return attribute_name
|
||||||
|
|
||||||
|
|
||||||
|
def expression_attribute_values_lookup(attribute_value, expression_attribute_values):
|
||||||
|
if isinstance(attribute_value, six.string_types) and \
|
||||||
|
attribute_value.startswith(':') and\
|
||||||
|
attribute_value in expression_attribute_values:
|
||||||
|
return expression_attribute_values[attribute_value]
|
||||||
|
else:
|
||||||
|
return attribute_value
|
||||||
|
|
||||||
|
|
||||||
class DynamoHandler(BaseResponse):
|
class DynamoHandler(BaseResponse):
|
||||||
|
|
||||||
def get_endpoint_name(self, headers):
|
def get_endpoint_name(self, headers):
|
||||||
@ -220,24 +282,13 @@ class DynamoHandler(BaseResponse):
|
|||||||
# expression
|
# expression
|
||||||
if not expected:
|
if not expected:
|
||||||
condition_expression = self.body.get('ConditionExpression')
|
condition_expression = self.body.get('ConditionExpression')
|
||||||
if condition_expression and 'OR' not in condition_expression:
|
expression_attribute_names = self.body.get('ExpressionAttributeNames', {})
|
||||||
cond_items = [c.strip()
|
expression_attribute_values = self.body.get('ExpressionAttributeValues', {})
|
||||||
for c in condition_expression.split('AND')]
|
expected = condition_expression_to_expected(condition_expression,
|
||||||
|
expression_attribute_names,
|
||||||
if cond_items:
|
expression_attribute_values)
|
||||||
expected = {}
|
if expected:
|
||||||
overwrite = False
|
overwrite = False
|
||||||
exists_re = re.compile('^attribute_exists\s*\((.*)\)$')
|
|
||||||
not_exists_re = re.compile(
|
|
||||||
'^attribute_not_exists\s*\((.*)\)$')
|
|
||||||
|
|
||||||
for cond in cond_items:
|
|
||||||
exists_m = exists_re.match(cond)
|
|
||||||
not_exists_m = not_exists_re.match(cond)
|
|
||||||
if exists_m:
|
|
||||||
expected[exists_m.group(1)] = {'Exists': True}
|
|
||||||
elif not_exists_m:
|
|
||||||
expected[not_exists_m.group(1)] = {'Exists': False}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.dynamodb_backend.put_item(name, item, expected, overwrite)
|
result = self.dynamodb_backend.put_item(name, item, expected, overwrite)
|
||||||
@ -590,23 +641,11 @@ class DynamoHandler(BaseResponse):
|
|||||||
# expression
|
# expression
|
||||||
if not expected:
|
if not expected:
|
||||||
condition_expression = self.body.get('ConditionExpression')
|
condition_expression = self.body.get('ConditionExpression')
|
||||||
if condition_expression and 'OR' not in condition_expression:
|
expression_attribute_names = self.body.get('ExpressionAttributeNames', {})
|
||||||
cond_items = [c.strip()
|
expression_attribute_values = self.body.get('ExpressionAttributeValues', {})
|
||||||
for c in condition_expression.split('AND')]
|
expected = condition_expression_to_expected(condition_expression,
|
||||||
|
expression_attribute_names,
|
||||||
if cond_items:
|
expression_attribute_values)
|
||||||
expected = {}
|
|
||||||
exists_re = re.compile('^attribute_exists\s*\((.*)\)$')
|
|
||||||
not_exists_re = re.compile(
|
|
||||||
'^attribute_not_exists\s*\((.*)\)$')
|
|
||||||
|
|
||||||
for cond in cond_items:
|
|
||||||
exists_m = exists_re.match(cond)
|
|
||||||
not_exists_m = not_exists_re.match(cond)
|
|
||||||
if exists_m:
|
|
||||||
expected[exists_m.group(1)] = {'Exists': True}
|
|
||||||
elif not_exists_m:
|
|
||||||
expected[not_exists_m.group(1)] = {'Exists': False}
|
|
||||||
|
|
||||||
# Support spaces between operators in an update expression
|
# Support spaces between operators in an update expression
|
||||||
# E.g. `a = b + c` -> `a=b+c`
|
# E.g. `a = b + c` -> `a=b+c`
|
||||||
|
@ -1505,3 +1505,110 @@ def test_dynamodb_streams_2():
|
|||||||
assert 'LatestStreamLabel' in resp['TableDescription']
|
assert 'LatestStreamLabel' in resp['TableDescription']
|
||||||
assert 'LatestStreamArn' in resp['TableDescription']
|
assert 'LatestStreamArn' in resp['TableDescription']
|
||||||
|
|
||||||
|
@mock_dynamodb2
|
||||||
|
def test_condition_expressions():
|
||||||
|
client = boto3.client('dynamodb', region_name='us-east-1')
|
||||||
|
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
|
||||||
|
|
||||||
|
# Create the DynamoDB table.
|
||||||
|
client.create_table(
|
||||||
|
TableName='test1',
|
||||||
|
AttributeDefinitions=[{'AttributeName': 'client', 'AttributeType': 'S'}, {'AttributeName': 'app', 'AttributeType': 'S'}],
|
||||||
|
KeySchema=[{'AttributeName': 'client', 'KeyType': 'HASH'}, {'AttributeName': 'app', 'KeyType': 'RANGE'}],
|
||||||
|
ProvisionedThroughput={'ReadCapacityUnits': 123, 'WriteCapacityUnits': 123}
|
||||||
|
)
|
||||||
|
client.put_item(
|
||||||
|
TableName='test1',
|
||||||
|
Item={
|
||||||
|
'client': {'S': 'client1'},
|
||||||
|
'app': {'S': 'app1'},
|
||||||
|
'match': {'S': 'match'},
|
||||||
|
'existing': {'S': 'existing'},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.put_item(
|
||||||
|
TableName='test1',
|
||||||
|
Item={
|
||||||
|
'client': {'S': 'client1'},
|
||||||
|
'app': {'S': 'app1'},
|
||||||
|
'match': {'S': 'match'},
|
||||||
|
'existing': {'S': 'existing'},
|
||||||
|
},
|
||||||
|
ConditionExpression='attribute_exists(#existing) AND attribute_not_exists(#nonexistent) AND #match = :match',
|
||||||
|
ExpressionAttributeNames={
|
||||||
|
'#existing': 'existing',
|
||||||
|
'#nonexistent': 'nope',
|
||||||
|
'#match': 'match',
|
||||||
|
},
|
||||||
|
ExpressionAttributeValues={
|
||||||
|
':match': {'S': 'match'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.put_item(
|
||||||
|
TableName='test1',
|
||||||
|
Item={
|
||||||
|
'client': {'S': 'client1'},
|
||||||
|
'app': {'S': 'app1'},
|
||||||
|
'match': {'S': 'match'},
|
||||||
|
'existing': {'S': 'existing'},
|
||||||
|
},
|
||||||
|
ConditionExpression='NOT(attribute_exists(#nonexistent1) AND attribute_exists(#nonexistent2))',
|
||||||
|
ExpressionAttributeNames={
|
||||||
|
'#nonexistent1': 'nope',
|
||||||
|
'#nonexistent2': 'nope2'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(client.exceptions.ConditionalCheckFailedException):
|
||||||
|
client.put_item(
|
||||||
|
TableName='test1',
|
||||||
|
Item={
|
||||||
|
'client': {'S': 'client1'},
|
||||||
|
'app': {'S': 'app1'},
|
||||||
|
'match': {'S': 'match'},
|
||||||
|
'existing': {'S': 'existing'},
|
||||||
|
},
|
||||||
|
ConditionExpression='attribute_exists(#nonexistent1) AND attribute_exists(#nonexistent2)',
|
||||||
|
ExpressionAttributeNames={
|
||||||
|
'#nonexistent1': 'nope',
|
||||||
|
'#nonexistent2': 'nope2'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(client.exceptions.ConditionalCheckFailedException):
|
||||||
|
client.put_item(
|
||||||
|
TableName='test1',
|
||||||
|
Item={
|
||||||
|
'client': {'S': 'client1'},
|
||||||
|
'app': {'S': 'app1'},
|
||||||
|
'match': {'S': 'match'},
|
||||||
|
'existing': {'S': 'existing'},
|
||||||
|
},
|
||||||
|
ConditionExpression='NOT(attribute_not_exists(#nonexistent1) AND attribute_not_exists(#nonexistent2))',
|
||||||
|
ExpressionAttributeNames={
|
||||||
|
'#nonexistent1': 'nope',
|
||||||
|
'#nonexistent2': 'nope2'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with assert_raises(client.exceptions.ConditionalCheckFailedException):
|
||||||
|
client.put_item(
|
||||||
|
TableName='test1',
|
||||||
|
Item={
|
||||||
|
'client': {'S': 'client1'},
|
||||||
|
'app': {'S': 'app1'},
|
||||||
|
'match': {'S': 'match'},
|
||||||
|
'existing': {'S': 'existing'},
|
||||||
|
},
|
||||||
|
ConditionExpression='attribute_exists(#existing) AND attribute_not_exists(#nonexistent) AND #match = :match',
|
||||||
|
ExpressionAttributeNames={
|
||||||
|
'#existing': 'existing',
|
||||||
|
'#nonexistent': 'nope',
|
||||||
|
'#match': 'match',
|
||||||
|
},
|
||||||
|
ExpressionAttributeValues={
|
||||||
|
':match': {'S': 'match2'}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user