Make alternator, nodetool and rest_api test directories as python packages. Move scylla-gdb to scylla_gdb and make it python package.
1927 lines
106 KiB
Python
1927 lines
106 KiB
Python
# Copyright 2019-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
# Tests for the ConditionExpression parameter which makes certain operations
|
|
# (PutItem, UpdateItem and DeleteItem) conditional on the existing attribute
|
|
# values. ConditionExpression is a newer and more powerful version of the
|
|
# older "Expected" syntax. That older syntax is tested by the separate
|
|
# test_expected.py. Many of the tests there are very similar to the ones
|
|
# included here.
|
|
|
|
# NOTE: In this file, we use the b'xyz' syntax to represent DynamoDB's binary
|
|
# values. This syntax works as expected only in Python3. In Python2 it
|
|
# appears to work, but the "b" is actually ignored and the result is a normal
|
|
# string 'xyz'. That means that we end up testing the string type instead of
|
|
# the binary type as intended. So this test can run on Python2 but doesn't
|
|
# cover testing binary types. The test should be run in Python3 to ensure full
|
|
# coverage.
|
|
|
|
import pytest
|
|
from botocore.exceptions import ClientError
|
|
from test.alternator.util import random_string
|
|
from sys import version_info
|
|
|
|
# A helper function for changing write isolation policies
|
|
def set_write_isolation(table, isolation):
|
|
got = table.meta.client.describe_table(TableName=table.name)['Table']
|
|
arn = got['TableArn']
|
|
tags = [
|
|
{
|
|
'Key': 'system:write_isolation',
|
|
'Value': isolation
|
|
}
|
|
]
|
|
table.meta.client.tag_resource(ResourceArn=arn, Tags=tags)
|
|
|
|
# A helper function to clear previous isolation tags
|
|
def clear_write_isolation(table):
|
|
got = table.meta.client.describe_table(TableName=table.name)['Table']
|
|
arn = got['TableArn']
|
|
table.meta.client.untag_resource(ResourceArn=arn, TagKeys=['system:write_isolation'])
|
|
|
|
# Most of the tests in this file check that the ConditionExpression
|
|
# parameter works for the UpdateItem operation. It should also work the
|
|
# same for the PutItem and DeleteItem operations, and we'll make a small
|
|
# effort to verify that at the end of the file.
|
|
|
|
# Somewhat pedantically, DynamoDB forbids using new-style ConditionExpression
|
|
# together with old-style AttributeUpdates... ConditionExpression can only be
|
|
# used with UpdateExpression.
|
|
def test_condition_expression_attribute_updates(test_table_s):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}},
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':oldval': 2})
|
|
|
|
# The following string of tests will test conditions composed of a single
|
|
# comparison of two attributes (as usual, each can be an attribute of the
|
|
# item or a constant from the request's ExpressionAttributeValues).
|
|
# All these tests involve top-level attribute - we'll test the possibility
|
|
# of directly-addressing nested attributes in separate tests below.
|
|
# Additional tests below will check additional functions, as well as
|
|
# applying boolean logic (AND, OR, NOT, parentheses) on simpler conditions.
|
|
# In each case we have tests for the "true" case of the condition, meaning
|
|
# that the condition evaluates to true and the update is supposed to happen,
|
|
# and the "false" case, where the condition evaluates to false, so the update
|
|
# doesn't happen and we get a ConditionalCheckFailedException instead.
|
|
|
|
# Test for ConditionExpression with operator "=" (equality check):
|
|
|
|
# Check successful comparisons for values of all known types.
|
|
# We test both the case comparing one of the item's attributes to an
|
|
# attribute from the request, and the case of comparing two different
|
|
# attributes of the same item (the latter case wasn't possible to express
|
|
# with Expected, and becomes possible with ConditionExpression).
|
|
def test_update_condition_eq_success(test_table_s):
|
|
p = random_string()
|
|
values = (1, "hello", True, b'xyz', None, ['hello', 42], {'hello': 'world'}, set(['hello', 'world']), set([1, 2, 3]), set([b'xyz', b'hi']))
|
|
i = 0
|
|
for val in values:
|
|
i = i + 1
|
|
print(val)
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': val, 'Action': 'PUT'},
|
|
'b': {'Value': val, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :i',
|
|
ConditionExpression='a = b',
|
|
ExpressionAttributeValues={':i': i})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == i
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET d = :i',
|
|
ConditionExpression='a = :val',
|
|
ExpressionAttributeValues={':i': i, ':val': val})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['d'] == i
|
|
|
|
# Comparing values of *different* types should always fail. Check all the
|
|
# combination of different types.
|
|
def test_update_condition_eq_different(test_table_s):
|
|
p = random_string()
|
|
values = (1, "hello", True, b'xyz', None, ['hello', 42], {'hello': 'world'}, set(['hello', 'world']), set([1, 2, 3]), set([b'xyz', b'hi']))
|
|
for val1 in values:
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': val1, 'Action': 'PUT'}})
|
|
for val2 in values:
|
|
print('testing {} {}'.format(val1, val2))
|
|
# Frustratingly, Python considers True == 1, so we have to use
|
|
# this ugly expression instead of the trivial val1 == val2
|
|
if (val1 is True and val2 is True) or (not val1 is True and not val2 is True and val1 == val2):
|
|
# Condition should succeed
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='a = :val2',
|
|
ExpressionAttributeValues={':val1': val1, ':val2': val2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == val2
|
|
else:
|
|
# Condition should fail (different types)
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='a = :val2',
|
|
ExpressionAttributeValues={':val1': val1, ':val2': val2})
|
|
|
|
# Also check an actual case of same type, but inequality.
|
|
def test_update_condition_eq_unequal(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':val1': 3, ':oldval': 2})
|
|
# If the attribute being compared doesn't exist, it's considered a failed
|
|
# condition, not an error:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q = :oldval',
|
|
ExpressionAttributeValues={':val1': 3, ':oldval': 2})
|
|
|
|
# In test_update_condition_eq_unequal() above we saw that a non-existent
|
|
# attribute is not "=" to a value. Here we check what happens when two
|
|
# non-existent attributes are checked for equality. It turns out, they should
|
|
# *not* be considered equal. In short, an unset attribute is never equal to
|
|
# anything - not even to another unset attribute.
|
|
# Reproduces issue #8511.
|
|
def test_update_condition_eq_two_unset(test_table_s):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q = z',
|
|
ExpressionAttributeValues={':val1': 2})
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q = z',
|
|
ExpressionAttributeValues={':val1': 3})
|
|
|
|
# Check that set equality is checked correctly. Unlike string equality (for
|
|
# example), it cannot be done with just naive string comparison of the JSON
|
|
# representation, and we need to allow for any order. (see issue #5021)
|
|
def test_update_condition_eq_set(test_table_s):
|
|
p = random_string()
|
|
# Because boto3 sorts the set values we give it, in order to generate a
|
|
# set with a different order, we need to build it incrementally.
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': set(['dog', 'chinchilla']), 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='ADD a :val1',
|
|
ExpressionAttributeValues={':val1': set(['cat', 'mouse'])})
|
|
# Sanity check - the attribute contains the set we think it does
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['chinchilla', 'cat', 'dog', 'mouse'])
|
|
# Now finally check that condition expression check knows the equality too.
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET b = :val1',
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':val1': 3, ':oldval': set(['chinchilla', 'cat', 'dog', 'mouse'])})
|
|
assert 'b' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
|
|
|
# The above test (test_update_condition_eq_set()) checked equality of simple
|
|
# set attributes. But an attributes can contain a nested document, where the
|
|
# set sits in a deep level (the set itself is a leaf in this hierarchy because
|
|
# it can only contain numbers, strings or bytes). We need to correctly support
|
|
# equality check in that case too.
|
|
# Reproduces issue #8514.
|
|
def test_update_condition_eq_nested_set(test_table_s):
|
|
p = random_string()
|
|
# Because boto3 sorts the set values we give it, in order to generate a
|
|
# set with a different order, we need to build it incrementally.
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': {'b': 'c', 'd': ['e', 'f', set(['g', 'h'])], 'i': set(['j', 'k'])}, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='ADD a.d[2] :val1, a.i :val2',
|
|
ExpressionAttributeValues={':val1': set(['l', 'm']), ':val2': set(['n', 'o'])})
|
|
# Sanity check - the attribute contains the set we think it does
|
|
expected = {'b': 'c', 'd': ['e', 'f', set(['g', 'h', 'l', 'm'])], 'i': set(['j', 'k', 'n', 'o'])}
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == expected
|
|
# Now finally check that condition expression check knows the equality too.
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET b = :val1',
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':val1': 3, ':oldval': expected})
|
|
assert 'b' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
|
# Check that equality can also fail, if the inner set differs
|
|
wrong = {'b': 'c', 'd': ['e', 'f', set(['g', 'h', 'l', 'bad'])], 'i': set(['j', 'k', 'n', 'o'])}
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET b = :val1',
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':val1': 4, ':oldval': wrong})
|
|
|
|
# Test for ConditionExpression with operator "<>" (non-equality)
|
|
def test_update_condition_ne(test_table_s):
|
|
p = random_string()
|
|
# We only check here one type of attributes (numbers), assuming that the
|
|
# inequality code calls the equality-check code which we checked in more
|
|
# detail above.
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :newval',
|
|
ConditionExpression='a <> :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :newval',
|
|
ConditionExpression='a <> b',
|
|
ExpressionAttributeValues={':newval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :newval',
|
|
ConditionExpression='a <> c',
|
|
ExpressionAttributeValues={':newval': 4})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
|
|
# If the types are different, this is considered "not equal":
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :newval',
|
|
ConditionExpression='a <> :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': "1"})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 2
|
|
|
|
# If the attribute does not exist at all, this is also considered "not equal":
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :newval',
|
|
ConditionExpression='z <> :oldval',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 3
|
|
|
|
# Check that set inequality is checked correctly. This reproduces the same
|
|
# bug #5021 that we reproduced above in test_update_condition_eq_set(), just
|
|
# that here we check the inequality operator instead of equality.
|
|
# Reproduces issue #8513.
|
|
def test_update_condition_ne_set(test_table_s):
|
|
p = random_string()
|
|
# Because boto3 sorts the set values we give it, in order to generate a
|
|
# set with a different order, we need to build it incrementally.
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': set(['dog', 'chinchilla']), 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='ADD a :val1',
|
|
ExpressionAttributeValues={':val1': set(['cat', 'mouse'])})
|
|
# Sanity check - the attribute contains the set we think it does
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == set(['chinchilla', 'cat', 'dog', 'mouse'])
|
|
# Now check that condition expression check knows there is no inequality
|
|
# here.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET b = :val1',
|
|
ConditionExpression='a <> :oldval',
|
|
ExpressionAttributeValues={':val1': 2, ':oldval': set(['chinchilla', 'cat', 'dog', 'mouse'])})
|
|
# As a sanity check, also check something which should be unequal:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET b = :val1',
|
|
ConditionExpression='a <> :oldval',
|
|
ExpressionAttributeValues={':val1': 3, ':oldval': set(['chinchilla', 'cat', 'dog', 'horse'])})
|
|
assert 'b' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']
|
|
|
|
# In test_update_condition_ne() above we saw that a non-existent attribute is
|
|
# "not equal" to any value. Here we check what happens when two non-existent
|
|
# attributes are checked for non-equality. It turns out, they are also
|
|
# considered "not equal". In short, an unset attribute is always "not equal" to
|
|
# anything - even to another unset attribute.
|
|
# Reproduces issue #8511.
|
|
def test_update_condition_ne_two_unset(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q <> z',
|
|
ExpressionAttributeValues={':val1': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q <> z',
|
|
ExpressionAttributeValues={':val1': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['a'] == 3
|
|
|
|
# Test for ConditionExpression with operator "<"
|
|
def test_update_condition_lt(test_table_s):
|
|
p = random_string()
|
|
# The < operator should work for string, number and binary types
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 'cat', 'Action': 'PUT'},
|
|
'c': {'Value': b'cat', 'Action': 'PUT'}})
|
|
# true cases:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b < :oldval',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval': 'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c < :oldval',
|
|
ExpressionAttributeValues={':newval': 4, ':oldval': b'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
# false cases:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 0})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'cat'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'aardvark'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': b'cat'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': b'aardvark'})
|
|
# If the types are different, this is also considered false
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If the attribute being compared doesn't even exist, this is also
|
|
# considered as a false condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='q < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval < q',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If a comparison parameter comes from a constant specified in the query,
|
|
# and it has a type not supported by the comparison (e.g., a list), it's
|
|
# not just a failed comparison - it is considered a ValidationException
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval < a',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
# However, if when the wrong type comes from an item attribute, not the
|
|
# query, the comparison is simply false - not a ValidationException.
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'x': {'Value': [1,2,3], 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='x < :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval < x',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
|
|
# In test_update_condition_lt() above we saw that a non-existent attribute is
|
|
# not "<" any value. Here we check what happens when two non-existent
|
|
# attributes are compared with "<". It turns out that the result of such
|
|
# comparison is also false.
|
|
# The same is true for other order operators - any order comparison involving
|
|
# one unset attribute should be false - even if the second operand is an
|
|
# unset attribute as well. Note that the <> operator is different - it is
|
|
# always results in true if one of the operands is an unset attribute (see
|
|
# test_update_condition_ne_two_unset() above).
|
|
# This test is related to issue #8511 (although it passed even before fixing
|
|
# that issue).
|
|
def test_update_condition_comparison_two_unset(test_table_s):
|
|
p = random_string()
|
|
ops = ['<', '<=', '>', '>=']
|
|
for op in ops:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q ' + op + ' z',
|
|
ExpressionAttributeValues={':val1': 2})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q between z and x',
|
|
ExpressionAttributeValues={':val1': 2})
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
for op in ops:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q ' + op + ' z',
|
|
ExpressionAttributeValues={':val1': 3})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET a = :val1',
|
|
ConditionExpression='q between z and x',
|
|
ExpressionAttributeValues={':val1': 2})
|
|
|
|
# Test for ConditionExpression with operator "<="
|
|
def test_update_condition_le(test_table_s):
|
|
p = random_string()
|
|
# The <= operator should work for string, number and binary types
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 'cat', 'Action': 'PUT'},
|
|
'c': {'Value': b'cat', 'Action': 'PUT'}})
|
|
# true cases:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a <= :oldval',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b <= :oldval',
|
|
ExpressionAttributeValues={':newval': 4, ':oldval': 'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b <= :oldval',
|
|
ExpressionAttributeValues={':newval': 5, ':oldval': 'cat'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c <= :oldval',
|
|
ExpressionAttributeValues={':newval': 6, ':oldval': b'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 6
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c <= :oldval',
|
|
ExpressionAttributeValues={':newval': 7, ':oldval': b'cat'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
|
# false cases:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 0})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'aardvark'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': b'aardvark'})
|
|
# If the types are different, this is also considered false
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If the attribute being compared doesn't even exist, this is also
|
|
# considered as a false condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='q <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval <= q',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If a comparison parameter comes from a constant specified in the query,
|
|
# and it has a type not supported by the comparison (e.g., a list), it's
|
|
# not just a failed comparison - it is considered a ValidationException
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval <= a',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
# However, if when the wrong type comes from an item attribute, not the
|
|
# query, the comparison is simply false - not a ValidationException.
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'x': {'Value': [1,2,3], 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='x <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval <= x',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
|
|
|
# Test for ConditionExpression with operator ">"
|
|
def test_update_condition_gt(test_table_s):
|
|
p = random_string()
|
|
# The > operator should work for string, number and binary types
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 'cat', 'Action': 'PUT'},
|
|
'c': {'Value': b'cat', 'Action': 'PUT'}})
|
|
# true cases:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 0})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b > :oldval',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval': 'aardvark'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c > :oldval',
|
|
ExpressionAttributeValues={':newval': 4, ':oldval': b'aardvark'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
# false cases:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 2})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'cat'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'dog'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': b'cat'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': b'dog'})
|
|
# If the types are different, this is also considered false
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If the attribute being compared doesn't even exist, this is also
|
|
# considered as a false condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='q > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval > q',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If a comparison parameter comes from a constant specified in the query,
|
|
# and it has a type not supported by the comparison (e.g., a list), it's
|
|
# not just a failed comparison - it is considered a ValidationException
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval > a',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
# However, if when the wrong type comes from an item attribute, not the
|
|
# query, the comparison is simply false - not a ValidationException.
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'x': {'Value': [1,2,3], 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='x > :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval > x',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
|
|
# Test for ConditionExpression with operator ">="
|
|
def test_update_condition_ge(test_table_s):
|
|
p = random_string()
|
|
# The >= operator should work for string, number and binary types
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 'cat', 'Action': 'PUT'},
|
|
'c': {'Value': b'cat', 'Action': 'PUT'}})
|
|
# true cases:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 0})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a >= :oldval',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b >= :oldval',
|
|
ExpressionAttributeValues={':newval': 4, ':oldval': 'aardvark'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b >= :oldval',
|
|
ExpressionAttributeValues={':newval': 5, ':oldval': 'cat'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c >= :oldval',
|
|
ExpressionAttributeValues={':newval': 6, ':oldval': b'aardvark'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 6
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c >= :oldval',
|
|
ExpressionAttributeValues={':newval': 7, ':oldval': b'cat'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
|
# false cases:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 2})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'dog'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': b'dog'})
|
|
# If the types are different, this is also considered false
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '0'})
|
|
# If the attribute being compared doesn't even exist, this is also
|
|
# considered as a false condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='q >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval >= q',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': '17'})
|
|
# If a comparison parameter comes from a constant specified in the query,
|
|
# and it has a type not supported by the comparison (e.g., a list), it's
|
|
# not just a failed comparison - it is considered a ValidationException
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval >= a',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': [1,2]})
|
|
# However, if when the wrong type comes from an item attribute, not the
|
|
# query, the comparison is simply false - not a ValidationException.
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'x': {'Value': [1,2,3], 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='x >= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression=':oldval >= x',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
|
|
|
# Test for ConditionExpression with ternary operator "BETWEEN" (checking
|
|
# if a value is between two others, equality included). The keywords
|
|
# "BETWEEN" and "AND" are case insensitive.
|
|
def test_update_condition_between(test_table_s):
|
|
p = random_string()
|
|
# The BETWEEN operator should work for string, number and binary types
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 'cat', 'Action': 'PUT'},
|
|
'c': {'Value': b'cat', 'Action': 'PUT'}})
|
|
# true cases:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': 0, ':oldval2': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval1': 1, ':oldval2': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 4, ':oldval1': 'aardvark', ':oldval2': 'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 5, ':oldval1': 'cat', ':oldval2': 'cat'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 6, ':oldval1': b'aardvark', ':oldval2': b'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 6
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 7, ':oldval1': b'cat', ':oldval2': b'cat'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
|
# All three operands of the BETWEEN operator can be attributes of the
|
|
# item:
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'x': {'Value': 0, 'Action': 'PUT'},
|
|
'y': {'Value': 2, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN x AND y',
|
|
ExpressionAttributeValues={':newval': 8})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 8
|
|
# The keywords "BETWEEN" and "AND" are case insensitive
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a between x and y',
|
|
ExpressionAttributeValues={':newval': 9})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 9
|
|
# false cases:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': 2, ':oldval2': 7})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': 'dog', ':oldval2': 'zebra'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='c BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': b'dog', ':oldval2': b'zebra'})
|
|
# If the types are different, this is also considered false
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': '0', ':oldval2': '2'})
|
|
# If the attribute being compared doesn't even exist, this is also
|
|
# considered as a false condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='q BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': b'dog', ':oldval2': b'zebra'})
|
|
# If and operand from the query, and it has a type not supported by the
|
|
# comparison (e.g., a list), it's not just a failed condition - it is
|
|
# considered a ValidationException
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': [1,2], ':oldval2': [2,3]})
|
|
# However, if when the wrong type comes from an item attribute, not the
|
|
# query, the comparison is simply false - not a ValidationException.
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'x': {'Value': [1,2,3], 'Action': 'PUT'},
|
|
'y': {'Value': [2,3,4], 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN x and y',
|
|
ExpressionAttributeValues={':newval': 2})
|
|
# If the two operands come from the query (":val" references) then if they
|
|
# have different types or the wrong order, this is a ValidationException.
|
|
# But if one or more of the operands come from the item, this only causes
|
|
# a false condition - not a ValidationException.
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': 2, ':oldval2': 1})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval1 AND :oldval2',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval1': 2, ':oldval2': 'dog'})
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={'two': {'Value': 2, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN two AND :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN :oldval AND two',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 3})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='a BETWEEN two AND :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 'dog'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 9
|
|
|
|
# Test for ConditionExpression with multi-operand operator "IN", checking
|
|
# whether a value is equal to one of possibly many values (up to 100 should
|
|
# be supported, according to the DynamoDB documentation).
|
|
def test_update_condition_in(test_table_s):
|
|
p = random_string()
|
|
|
|
# The "IN" operator checks equality, and should work for any type.
|
|
# Here we just try the trivial successful equality check of one value:
|
|
values = (1, "hello", True, b'xyz', None, ['hello', 42], {'hello': 'world'}, set(['hello', 'world']), set([1, 2, 3]), set([b'xyz', b'hi']))
|
|
i = 0
|
|
for val in values:
|
|
i = i + 1
|
|
print(val)
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': val, 'Action': 'PUT'},
|
|
'b': {'Value': val, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :i',
|
|
ConditionExpression='a IN (b)',
|
|
ExpressionAttributeValues={':i': i})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == i
|
|
|
|
# The DynamoDB documentation suggests that IN's list can have up to 100
|
|
# attributes listed, but it actually supports only 99 (100 including
|
|
# the first argument to the operator), so let's check 99 work.
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 74, 'Action': 'PUT'}})
|
|
values = {':val{}'.format(i): i for i in range(99)}
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val37',
|
|
ConditionExpression='a IN ({})'.format(','.join(values.keys())),
|
|
ExpressionAttributeValues=values)
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 37
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 174, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException.*'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val37',
|
|
ConditionExpression='a IN ({})'.format(','.join(values.keys())),
|
|
ExpressionAttributeValues=values)
|
|
# Unlike the IN operation in Expected, here it is not a validation error
|
|
# for the different values to have different types (of course, the
|
|
# condition will only end up succeeding if one of the listed values has
|
|
# the correct type - and value.
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='a IN (:x, :y)',
|
|
ExpressionAttributeValues={':val': 1, ':x': 'dog', ':y': 174})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
# IN with zero arguments results in a syntax error, not a failed condition
|
|
with pytest.raises(ClientError, match='ValidationException.*yntax error'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val37',
|
|
ConditionExpression='a IN ()',
|
|
ExpressionAttributeValues=values)
|
|
# If the attribute being compared doesn't even exist, this is also
|
|
# considered as a false condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val37',
|
|
ConditionExpression='q IN ({})'.format(','.join(values.keys())),
|
|
ExpressionAttributeValues=values)
|
|
|
|
# Beyond the above operators, there are also test functions supported -
|
|
# attribute_exists, attribute_not_exists, attribute_type, begins_with,
|
|
# contains, and size (these function names are case sensitive).
|
|
# These functions are listed and described in
|
|
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
|
|
|
def test_update_condition_attribute_exists(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_exists (a)',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException.*'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_exists (z)',
|
|
ExpressionAttributeValues={':val': 3})
|
|
# Somewhat artificially, attribute_exists() requires that its parameter
|
|
# be a path - it cannot be a different sort of value.
|
|
with pytest.raises(ClientError, match='ValidationException.*path'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_exists (:val)',
|
|
ExpressionAttributeValues={':val': 3})
|
|
|
|
# Primitive conditions usually look like an operator between two (<, <=,
|
|
# etc.), three (BETWEEN) or more (IN) values. Can just a single value be
|
|
# a condition? The special case of a single function call *can* be - we saw
|
|
# an example attribute_exists(z) in the previous test. However only
|
|
# function calls are supported in this context - not general values (i.e.,
|
|
# attribute or value references).
|
|
# While DynamoDB does not accept a non-function-call value as a condition
|
|
# (it results with with a syntax error), in Alternator currently, for
|
|
# simplicity of the parser, this case is parsed correctly and only fails
|
|
# later when the calculated value ends up to not be a boolean.
|
|
def test_update_condition_single_value_attribute(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='a',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_update_condition_attribute_not_exists(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_not_exists (b)',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException.*'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_not_exists (a)',
|
|
ExpressionAttributeValues={':val': 3})
|
|
|
|
def test_update_condition_attribute_type(test_table_s):
|
|
p = random_string()
|
|
type_values = [
|
|
('S', 'hello'),
|
|
('SS', set(['hello', 'world'])),
|
|
('N', 2),
|
|
('NS', set([1, 2])),
|
|
('B', b'dog'),
|
|
('BS', set([b'dog', b'cat'])),
|
|
('BOOL', True),
|
|
('NULL', None),
|
|
('L', [1, 'dog']),
|
|
('M', {'a': 3, 'b': 4})]
|
|
updates={'a{}'.format(i): {'Value': type_values[i][1], 'Action': 'PUT'} for i in range(len(type_values))}
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates=updates)
|
|
for i in range(len(type_values)):
|
|
expected_type = type_values[i][0]
|
|
# As explained in a comment in the top of the file, the binary types
|
|
# cannot be tested with Python 2
|
|
if expected_type in ('B', 'BS') and version_info[0] == 2:
|
|
continue
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_type (a{}, :type)'.format(i),
|
|
ExpressionAttributeValues={':val': i, ':type': expected_type})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == i
|
|
wrong_type = type_values[(i + 1) % len(type_values)][0]
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException.*'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_type (a{}, :type)'.format(i),
|
|
ExpressionAttributeValues={':val': i, ':type': wrong_type})
|
|
# The DynamoDB documentation suggests that attribute_type()'s first
|
|
# parameter must be a path (as we saw above, this is indeed the case for
|
|
# attribute_exists()). But in fact, attribute_type() does work fine also
|
|
# for an expression attribute.
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_type (:val, :type)',
|
|
ExpressionAttributeValues={':val': 0, ':type': 'N'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 0
|
|
|
|
# The DynamoDB documentation explicitly states that the second argument
|
|
# of the attribute_type function - the type to compare to - *must* be an
|
|
# expression attribute (:name) - it cannot be an item attribute.
|
|
# I don't know why this was important to forbid, but this test confirms that
|
|
# DynamoDB does forbid it.
|
|
def test_update_condition_attribute_type_second_arg(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 'N', 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_type (a, b)',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# If the attribute_type() parameter is not one of the known types
|
|
# (N,NS,BS,L,SS,NULL,B,BOOL,S,M), an error is generated. We should
|
|
# not get a failed condition.
|
|
def test_update_condition_attribute_type_unknown(test_table_s):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException.*DOG'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_type (a, :type)',
|
|
ExpressionAttributeValues={':val': 1, ':type': 'DOG'})
|
|
|
|
def test_update_condition_begins_with(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'},
|
|
'b': {'Value': b'hi there', 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(a, :arg)',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 'hell'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(b, :arg)',
|
|
ExpressionAttributeValues={':val': 2, ':arg': b'hi'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 2
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException.*'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(a, :arg)',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 'dog'})
|
|
# begins_with() requires String or Binary operand, giving it a number
|
|
# inside the expression results with a ValidationException (not a normal
|
|
# failed condition):
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(a, :arg)',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 2})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(c, :arg)',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 2})
|
|
# However, that extra type check is only done on values inside the
|
|
# expression. It isn't done on values from an item attributes - in that
|
|
# case we got a normal failed condition.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(c, :arg)',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 'dog'})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(c, a)',
|
|
ExpressionAttributeValues={':val': 3})
|
|
# Although the DynamoDB documentation suggests that begins_with()
|
|
# can only take a path as the first parameter and a constant as
|
|
# the second, this isn't actually true - begins_with() works
|
|
# as expected also to compare two attributes, or in reverse order:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(:str, a)',
|
|
ExpressionAttributeValues={':val': 'he', ':str': 'hellohi'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 'he'
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='begins_with(a, c)',
|
|
ExpressionAttributeValues={':val': 5})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 5
|
|
|
|
|
|
def test_update_condition_contains(test_table_s):
|
|
p = random_string()
|
|
# contains() can be used for two unrelated things: check substring (in
|
|
# string or binary) and membership (in set or a list). The DynamoDB
|
|
# documentation only mention string and set (not binary or list) but
|
|
# the fact is that binary and list are also support.
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'},
|
|
'b': {'Value': set([2, 4, 7]), 'Action': 'PUT'},
|
|
'c': {'Value': [2, 4, 7], 'Action': 'PUT'},
|
|
'd': {'Value': b'hi there', 'Action': 'PUT'},
|
|
'e': {'Value': ['hi', set([1,2]), [3, 4]], 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(a, :arg)',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 'ell'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
# The DynamoDB documentation incorrectly states that the second operand
|
|
# must always be a string. That's not true - it's fine to test if a
|
|
# set of numbers contains a number, for example.
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(b, :arg)',
|
|
ExpressionAttributeValues={':val': 2, ':arg': 4})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(c, :arg)',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 4})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(d, :arg)',
|
|
ExpressionAttributeValues={':val': 4, ':arg': b'here'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(d, :arg)',
|
|
ExpressionAttributeValues={':val': 4, ':arg': b'dog'})
|
|
# Moreover, the second parameter to contains() may be *any* type, and
|
|
# contains checks if perhaps the first parameter is a list or a set
|
|
# containing that value!
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(d, :arg)',
|
|
ExpressionAttributeValues={':val': 4, ':arg': set([1, 2])})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(e, :arg)',
|
|
ExpressionAttributeValues={':val': 5, ':arg': set([1, 2])})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(e, :arg)',
|
|
ExpressionAttributeValues={':val': 6, ':arg': [3, 4]})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 6
|
|
|
|
|
|
# While both operands of contains() may be item attributes, strangely
|
|
# it is explicitly forbidden to have the same attribute as both and
|
|
# trying to do so results in a ValidationException. I don't know why it's
|
|
# important to make this query fail, when it could have just worked...
|
|
# TODO: Is this limitation only for contains() or other functions as well?
|
|
@pytest.mark.xfail(reason="extra check for same attribute not implemented yet")
|
|
def test_update_condition_contains_same_attribute(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a1': {'Value': 'hello', 'Action': 'PUT'},
|
|
'a': {'Value': 'hello', 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(a, a1)',
|
|
ExpressionAttributeValues={':val': 5})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='contains(a, a)',
|
|
ExpressionAttributeValues={':val': 5})
|
|
|
|
# The syntax of the size() function is is incorrectly specified in
|
|
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
|
# size() itself is not a boolean function as shown there, but rather a numeric
|
|
# function whose return value needs to be further combined with another
|
|
# operand using a comparison operation - and it isn't specified which is
|
|
# supported.
|
|
def test_update_condition_size(test_table_s):
|
|
p = random_string()
|
|
# First verify what size() returns for various types. We use only the
|
|
# "=" comparison for these tests:
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'},
|
|
'b': {'Value': set([2, 4, 7]), 'Action': 'PUT'},
|
|
'c': {'Value': [2, 'dog', 7], 'Action': 'PUT'},
|
|
'd': {'Value': b'hi there', 'Action': 'PUT'},
|
|
'e': {'Value': {'x': 2, 'y': {'m': 3, 'n': 4}}, 'Action': 'PUT'},
|
|
'f': {'Value': 5, 'Action': 'PUT'},
|
|
'g': {'Value': True, 'Action': 'PUT'},
|
|
'h': {'Value': None, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)=:arg',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(b)=:arg',
|
|
ExpressionAttributeValues={':val': 2, ':arg': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(c)=:arg',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(d)=:arg',
|
|
ExpressionAttributeValues={':val': 4, ':arg': 8})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(e)=:arg',
|
|
ExpressionAttributeValues={':val': 5, ':arg': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(f)=:arg',
|
|
ExpressionAttributeValues={':val': 6, ':arg': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(g)=:arg',
|
|
ExpressionAttributeValues={':val': 6, ':arg': 1})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(h)=:arg',
|
|
ExpressionAttributeValues={':val': 6, ':arg': 1})
|
|
# Trying to compare the size() to a non-number results in a normal
|
|
# condition failure, not a ValidationException.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)=:arg',
|
|
ExpressionAttributeValues={':val': 6, ':arg': 'dog'})
|
|
# The argument to which the size is being compared to *may* be one of the
|
|
# item attributes too:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)=f',
|
|
ExpressionAttributeValues={':val': 6})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 6
|
|
# After testing the "=" operator thoroughly, check other operators are also
|
|
# supported.
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)<>:arg',
|
|
ExpressionAttributeValues={':val': 7, ':arg': 4})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 7
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)<:arg',
|
|
ExpressionAttributeValues={':val': 8, ':arg': 7})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 8
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)<=:arg',
|
|
ExpressionAttributeValues={':val': 9, ':arg': 7})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 9
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)>:arg',
|
|
ExpressionAttributeValues={':val': 10, ':arg': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 10
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)>=:arg',
|
|
ExpressionAttributeValues={':val': 11, ':arg': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 11
|
|
# size() is only allowed one operand; More operands are allowed by the
|
|
# parser, but later result in an error:
|
|
with pytest.raises(ClientError, match='ValidationException.*2'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a, a)=:arg',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
|
|
|
# The documentation claims that the size() function requires a path parameter
|
|
# so we check that both direct and reference paths work. But it turns out
|
|
# that size() can *also* be run on values set in the query.
|
|
# Reproduces #14592.
|
|
def test_update_condition_size_parameter(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'}})
|
|
# size(a) - works:
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)>=:arg',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
# size(#zyz) - works
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(#xyz)>=:arg',
|
|
ExpressionAttributeNames={'#xyz': 'a'},
|
|
ExpressionAttributeValues={':val': 2, ':arg': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
# size(:xyz) returns the size of the value defined as :xyz, it does NOT
|
|
# take the value of :xyz as referring to the path! If the value :xyz
|
|
# is a string, its size is defined. But it's an integer, it's not.
|
|
# Because the error is in the query (not the data from the database),
|
|
# it generates a ValidationException in this case, *not* a
|
|
# ConditionalCheckFailedException. This is different from the case we
|
|
# tested above in test_update_condition_size, of invalid type in the
|
|
# database. The error ValidationException message is "Invalid
|
|
# ConditionExpression: Incorrect operand type for operator or function;
|
|
# operator or function: size, operand type: N".
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(:xyz)>=:arg',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 2, ':xyz': 'abc'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(:xyz)>=:arg',
|
|
ExpressionAttributeValues={':val': 3, ':arg': 2, ':xyz': 123})
|
|
# Similarly, size(size(a)) is a ValidationException as well - because
|
|
# size(a) is a number, for which size() is not defined.
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(size(a))>=:arg',
|
|
ExpressionAttributeValues={':val': 2, ':arg': 2})
|
|
|
|
# The above test tested conditions involving size() in a comparison.
|
|
# Trying to use just size(a) as a condition (as we use the rest of the
|
|
# functions supported by ConditionExpression) does not work - DynamoDB
|
|
# reports that "The function is not allowed to be used this way in an
|
|
# expression; function: size".
|
|
def test_update_condition_size_alone(test_table_s):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='size(a)',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# Similarly, while attribute_exists(a) works alone, it cannot be used in
|
|
# a comparison, e.g., attribute_exists(a) < 1 also causes DynamoDB to
|
|
# complain about "The function is not allowed to be used in this way in an
|
|
# expression.".
|
|
def test_update_condition_attribute_exists_in_comparison(test_table_s):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='attribute_exists(a) < :val',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# In essence, the size() function tested in the previous test behaves
|
|
# exactly like the functions of UpdateExpressions, i.e., it transforms a
|
|
# value (attribute from the item or the query) into a new value, which
|
|
# can then be operated (in our case, compared). In this test we check
|
|
# that other functions supported by UpdateExpression - if_not_exists()
|
|
# and list_append() - are not supported.
|
|
def test_update_condition_other_funcs(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 'hello', 'Action': 'PUT'}})
|
|
# dog() is an unknown function name:
|
|
with pytest.raises(ClientError, match='ValidationException.*function'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='dog(a)=:arg',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
|
# The functions if_not_exists() and list_append() are known functions
|
|
# (they are supported in UpdateExpression) but not allowed in
|
|
# ConditionExpression. This means we can have a single function for
|
|
# evaluation a parsed::value, but it needs to know whether it is
|
|
# called for a UpdateExpression or a ConditionExpression.
|
|
with pytest.raises(ClientError, match='ValidationException.*not allowed'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='if_not_exists(a, a)=:arg',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
|
with pytest.raises(ClientError, match='ValidationException.*not allowed'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='list_append(a, a)=:arg',
|
|
ExpressionAttributeValues={':val': 1, ':arg': 5})
|
|
|
|
# All the previous tests involved top-level attributes to be tested. But
|
|
# ConditionExpressions also allows reading nested attributes, and we should
|
|
# support that too. This test just checks a few random operators - we don't
|
|
# test all the different operators here.
|
|
def test_update_condition_nested_attributes(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'b': {'Value': {'x': 1, 'y': [-1, 2, 0]}, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_exists (b.x)',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='b.x < b.y[1]',
|
|
ExpressionAttributeValues={':val': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 2
|
|
# Also check the case of a failing condition
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='b.x < b.y[0]',
|
|
ExpressionAttributeValues={':val': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 2
|
|
# A condition involving an attribute which doesn't exist results in
|
|
# failed condition - not an error.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='b.z < b.y[100]',
|
|
ExpressionAttributeValues={':val': 4})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 2
|
|
|
|
# All the previous tests referred to attributes using their name directly.
|
|
# But the DynamoDB API also allows to refer to attributes using a #reference.
|
|
# Among other things this allows using attribute names which are usually
|
|
# reserved keywords in condition expressions.
|
|
def test_update_condition_attribute_reference(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'and': {'Value': 1, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='attribute_exists (#name)',
|
|
ExpressionAttributeNames={'#name': 'and'},
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
|
|
def test_update_condition_nested_attribute_reference(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'and': {'Value': {'or': 2}, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET c = :val',
|
|
ConditionExpression='#name1.#name2 = :two',
|
|
ExpressionAttributeNames={'#name1': 'and', '#name2': 'or'},
|
|
ExpressionAttributeValues={':val': 1, ':two': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['c'] == 1
|
|
|
|
# All the previous tests involved a single condition. The following tests
|
|
# involve building more complex conditions by using AND, OR, NOT and
|
|
# parentheses on simpler condition expressions. There's also operator
|
|
# precedence involved, and should be tested (see the definitions in
|
|
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
|
|
|
|
def test_update_condition_and(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'},
|
|
'c': {'Value': 3, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='a < b AND b < c',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
# The "AND" keyword is case insensitive
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='a < b aNd b < c',
|
|
ExpressionAttributeValues={':val': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
# A failed "AND" condition:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='a < b AND c < b',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_update_condition_or(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'},
|
|
'c': {'Value': 3, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='a < b OR b < c',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='b < a OR b < c',
|
|
ExpressionAttributeValues={':val': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
# The "OR" keyword is case insensitive
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='a < b oR b < c',
|
|
ExpressionAttributeValues={':val': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
# A failed "OR" condition:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='b < a OR c < b',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_update_condition_not(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='NOT b < a',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
# The "NOT" keyword is case insensitive
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='nOt b < a',
|
|
ExpressionAttributeValues={':val': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
# A failed "NOT" condition:
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='NOT a < b',
|
|
ExpressionAttributeValues={':val': 1})
|
|
# NOT NOT NOT NOT also works (and does nothing) :-)
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='NOT NOT NOT NOT a < b',
|
|
ExpressionAttributeValues={':val': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
|
|
def test_update_condition_parentheses(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'},
|
|
'c': {'Value': 3, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='(a < b OR b < a) AND (b < c AND (a < b OR b < c))',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
|
|
# There is operator precedence that allows a user to use less parentheses.
|
|
# We need to implement it correctly:
|
|
|
|
def test_update_condition_and_before_or(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'},
|
|
'c': {'Value': 3, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='a < b OR c < b AND b < c',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
|
|
def test_update_condition_not_before_and(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'},
|
|
'c': {'Value': 3, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='NOT a < b AND c < b',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_update_condition_between_before_and(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'},
|
|
'c': {'Value': 3, 'Action': 'PUT'}})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='b BETWEEN a AND c AND a < b',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
|
|
# An empty ConditionExpression is not allowed - resulting in a validation
|
|
# error, not a failed condition:
|
|
def test_update_condition_empty(test_table_s):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression='',
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# All of the above tests tested ConditionExpression with the UpdateItem
|
|
# operation. We now want to test that it works also with the PutItem and
|
|
# DeleteItems operations. We don't need to check again all the different
|
|
# sub-cases tested above - we can assume that exactly the same code gets
|
|
# used to test the condition. So we just need one test for each operation,
|
|
# to verify that this code actually gets called.
|
|
|
|
def test_delete_item_condition(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.delete_item(Key={'p': p},
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':oldval': 2})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
|
|
test_table_s.delete_item(Key={'p': p},
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':oldval': 1})
|
|
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
|
|
def test_put_item_condition(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
test_table_s.put_item(Item={'p': p, 'a': 2},
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':oldval': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.put_item(Item={'p': p, 'a': 3},
|
|
ConditionExpression='a = :oldval',
|
|
ExpressionAttributeValues={':oldval': 1})
|
|
|
|
# DynamoDB frowns upon unused entries in ExpressionAttributeValues and
|
|
# ExpressionAttributeNames. Check that we do too (in all three operations),
|
|
# although it's not terribly important that we be compatible with DynamoDB
|
|
# here...
|
|
# There's one delicate issue, though. Should we check for unused entries
|
|
# during parsing, or during evaluation? The stage we check this changes
|
|
# our behavior when the condition was supposed to fail. So we have two
|
|
# separate tests here, one for failed condition and one for successful.
|
|
def test_update_condition_unused_entries_failed(test_table_s):
|
|
p = random_string()
|
|
# unused val3:
|
|
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET #name1 = :val1',
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val1': 1, ':val2': 2, ':val3': 3},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b'})
|
|
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
|
test_table_s.delete_item(Key={'p': p},
|
|
ConditionExpression='#name1 = :val1',
|
|
ExpressionAttributeValues={':val1': 1, ':val3': 3},
|
|
ExpressionAttributeNames={'#name1': 'a'})
|
|
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
|
test_table_s.put_item(Item={'p': p, 'a': 3},
|
|
ConditionExpression='#name1 = :val1',
|
|
ExpressionAttributeValues={':val1': 1, ':val3': 3},
|
|
ExpressionAttributeNames={'#name1': 'a'})
|
|
# unused name3:
|
|
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET #name1 = :val1',
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val1': 1, ':val2': 2},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b', '#name3': 'c'})
|
|
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
|
test_table_s.delete_item(Key={'p': p},
|
|
ConditionExpression='#name1 = :val1',
|
|
ExpressionAttributeValues={':val1': 1},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name3': 'c'})
|
|
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
|
test_table_s.put_item(Item={'p': p, 'a': 3},
|
|
ConditionExpression='#name1 = :val1',
|
|
ExpressionAttributeValues={':val1': 1},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name3': 'c'})
|
|
def test_update_condition_unused_entries_succeeded(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'b': {'Value': 2, 'Action': 'PUT'}})
|
|
# unused val3:
|
|
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET #name1 = :val1',
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val1': 1, ':val2': 2, ':val3': 3},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b'})
|
|
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
|
test_table_s.delete_item(Key={'p': p},
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val2': 2, ':val3': 3},
|
|
ExpressionAttributeNames={'#name2': 'b'})
|
|
with pytest.raises(ClientError, match='ValidationException.*val3'):
|
|
test_table_s.put_item(Item={'p': p, 'a': 3},
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val2': 2, ':val3': 3},
|
|
ExpressionAttributeNames={'#name2': 'b'})
|
|
# unused name3:
|
|
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET #name1 = :val1',
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val1': 1, ':val2': 2},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b', '#name3': 'c'})
|
|
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
|
test_table_s.delete_item(Key={'p': p},
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val2': 2},
|
|
ExpressionAttributeNames={'#name2': 'b', '#name3': 'c'})
|
|
with pytest.raises(ClientError, match='ValidationException.*name3'):
|
|
test_table_s.put_item(Item={'p': p, 'a': 3},
|
|
ConditionExpression='#name2 = :val2',
|
|
ExpressionAttributeValues={':val2': 2},
|
|
ExpressionAttributeNames={'#name2': 'b', '#name3': 'c'})
|
|
|
|
# Another reason why we must test for used references right after parsing
|
|
# the expressions, NOT at evaluation time, is that in some cases evaluation
|
|
# may short-circuit and not reach certain parts of the expression, and as
|
|
# a result we may wrongly think some names were not used, and refuse a
|
|
# perfectly good request. Such a bug (see issue #6572) can be fixed by
|
|
# either by dropping short-circuit evaluation (i.e., evaluate all parts
|
|
# of the expression even if the first OR succeeds), or by testing for
|
|
# unused references before evaluating anything.
|
|
def test_update_condition_unused_entries_short_circuit(test_table_s):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'}})
|
|
# If short-circuit evaluation is done for ConditionExpression, it will
|
|
# not use #name2 or :val2. But we shouldn't fail this request claiming
|
|
# these references weren't used... They were used in the expression,
|
|
# just not in the evaluation. This request *should* work.
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='#name1 = :val1 OR #name2 = :val2',
|
|
UpdateExpression='SET #name1 = :val3',
|
|
ExpressionAttributeValues={':val1': 1, ':val2': 2, ':val3': 3},
|
|
ExpressionAttributeNames={'#name1': 'a', '#name2': 'b'})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 3}
|
|
|
|
# Test a bunch of cases with permissive write isolation levels,
|
|
# i.e. LWT_ALWAYS, LWT_RMW_ONLY and UNSAFE_RMW.
|
|
# These test cases make sense only for alternator, so they're skipped
|
|
# when run on AWS
|
|
def test_condition_expression_with_permissive_write_isolation(scylla_only, dynamodb, test_table_s):
|
|
try:
|
|
for isolation in ['a', 'o', 'u']:
|
|
set_write_isolation(test_table_s, isolation)
|
|
for test_case in [test_update_condition_eq_success,
|
|
test_update_condition_attribute_exists,
|
|
test_delete_item_condition,
|
|
test_put_item_condition,
|
|
test_update_condition_attribute_reference]:
|
|
test_case(test_table_s)
|
|
finally:
|
|
clear_write_isolation(test_table_s)
|
|
|
|
# Test that the forbid_rmw isolation level prevents read-modify-write requests
|
|
# from working. These test cases make sense only for alternator, so they're skipped
|
|
# when run on AWS (test_table_s_forbid_rmw implies scylla_only)
|
|
def test_condition_expression_with_forbidden_rmw(dynamodb, test_table_s_forbid_rmw):
|
|
for test_case in [test_update_condition_eq_success, test_update_condition_attribute_exists,
|
|
test_put_item_condition, test_update_condition_attribute_reference]:
|
|
with pytest.raises(ClientError):
|
|
test_case(test_table_s_forbid_rmw)
|
|
# Ensure that regular writes (without rmw) work just fine
|
|
s = random_string()
|
|
test_table_s_forbid_rmw.put_item(Item={'p': s, 'regular': 'write'})
|
|
assert test_table_s_forbid_rmw.get_item(Key={'p': s}, ConsistentRead=True)['Item'] == {'p': s, 'regular': 'write'}
|
|
test_table_s_forbid_rmw.update_item(Key={'p': s}, AttributeUpdates={'write': {'Value': 'regular', 'Action': 'PUT'}})
|
|
assert test_table_s_forbid_rmw.get_item(Key={'p': s}, ConsistentRead=True)['Item'] == {'p': s, 'regular': 'write', 'write': 'regular'}
|
|
|
|
# Reproducer for issue #6573: binary strings should be ordered as unsigned
|
|
# bytes, i.e., byte 128 comes after 127, not before as with signed bytes.
|
|
# Test the five ordering operators: <, <=, >, >=, between
|
|
def test_condition_expression_unsigned_bytes(test_table_s):
|
|
p = random_string()
|
|
test_table_s.put_item(Item={'p': p, 'b': bytearray([127])})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b < :oldval',
|
|
ExpressionAttributeValues={':newval': 1, ':oldval': bytearray([128])})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b <= :oldval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': bytearray([128])})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 2
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b between :oldval1 and :oldval2',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval1': bytearray([126]), ':oldval2': bytearray([128])})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 3
|
|
|
|
test_table_s.put_item(Item={'p': p, 'b': bytearray([128])})
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b > :oldval',
|
|
ExpressionAttributeValues={':newval': 4, ':oldval': bytearray([127])})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 4
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :newval',
|
|
ConditionExpression='b >= :oldval',
|
|
ExpressionAttributeValues={':newval': 5, ':oldval': bytearray([127])})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 5
|
|
|
|
# In all other tests above, we use ConditionExpression to check a condition
|
|
# on one the non-key attributes. In this test we confirm that a condition may
|
|
# also be on a key attribute. We demonstrate this through a useful DynamoDB
|
|
# idiom for creating an item unless an item already exists with the same key,
|
|
# by using a "<>" (not equal) condition.
|
|
def test_update_item_condition_key_ne(test_table_s):
|
|
p = random_string()
|
|
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
# Create an empty item with key p, but only if no item with p exists yet.
|
|
# Note how when the item does not exist, the <> (not equal) test succeeds
|
|
# (we already tested that in test_update_condition_ne())
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='p <> :p',
|
|
ExpressionAttributeValues={':p': p})
|
|
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
# If we do the same again, the item does exist, and the <> condition will
|
|
# fail.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='p <> :p',
|
|
ExpressionAttributeValues={':p': p})
|
|
|
|
# Another example of a condition on the key, again an idiom for creating an
|
|
# item if no item already has that key. This time, using the
|
|
# attribute_not_exists() instead of the <> (not equal) operator in the test
|
|
# above.
|
|
def test_update_item_condition_key_attribute_not_exists(test_table_s):
|
|
p = random_string()
|
|
assert not 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
# Create an empty item with key p, but only an item with p exists yet.
|
|
# Note how when the item does not exist, attribute_not_exists() succeeds
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='attribute_not_exists(p)')
|
|
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
# If we do the same again, the item does exist, and the
|
|
# attribute_not_exists() condition will fail.
|
|
with pytest.raises(ClientError, match='ConditionalCheckFailedException'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='attribute_not_exists(p)')
|
|
|
|
# DynamoDB considers duplicate parentheses in expressions, which it calls
|
|
# "redundant parentheses", to be illegal. Outlawing them is also useful to
|
|
# avoid very deep recursion in the parser (see test_limits.py).
|
|
# Let's test here what is considered redendant parentheses, and what isn't.
|
|
@pytest.mark.xfail(reason="Alternator doesn't forbid redundant parentheses")
|
|
def test_redundant_parentheses(test_table_s):
|
|
# Putting one set of unnecessary parentheses is fine - e.g., "(p<>p)"
|
|
# works just as well as "p<>p" - it isn't considered "redundant
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='p <> :p',
|
|
ExpressionAttributeValues={':p': p})
|
|
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='(p <> :p)',
|
|
ExpressionAttributeValues={':p': p})
|
|
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|
|
# But putting two sets of parentheses, "((p<>p))", is considered redundant.
|
|
# DynamoDB prints: "Invalid ConditionExpression: The expression has
|
|
# redundant parentheses".
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException.*redundant parentheses'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='((p <> :p))',
|
|
ExpressionAttributeValues={':p': p})
|
|
# The expression "((p<>p) and p<>p)" isn't considered to have redundant
|
|
# parentheses - it's just like one unnecessary parentheses which we showed
|
|
# above is allowed. So the parser can't just claim "redundant parentheses"
|
|
# when it sees two successive parentheses beginning the expression.
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
ConditionExpression='((p <> :p) and p <> :p)',
|
|
ExpressionAttributeValues={':p': p})
|
|
assert 'Item' in test_table_s.get_item(Key={'p': p}, ConsistentRead=True)
|