This patch includes minor refactoring of expressions related tests (#22494) - use `test_table_ss` instead of `test_table`.
556 lines
31 KiB
Python
556 lines
31 KiB
Python
# Copyright 2020-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
|
|
# Tests for the KeyConditionExpression parameter of the Query operation.
|
|
# KeyConditionExpression is a newer version of the older "KeyConditions"
|
|
# syntax. That older syntax is tested in test_query.py and test_key_conditions.py.
|
|
|
|
import pytest
|
|
from boto3.dynamodb.conditions import Key
|
|
from botocore.exceptions import ClientError
|
|
|
|
from test.alternator.util import random_string, full_query, multiset
|
|
|
|
|
|
# The test_table_{sn,ss,sb}_with_sorted_partition fixtures are the regular
|
|
# test_table_{sn,ss,sb} fixture with a partition inserted with many items.
|
|
# The table, the partition key, and the items are returned - the items are
|
|
# sorted (by column 'c'), so they can be easily used to test various range
|
|
# queries. This fixture is useful for writing many small query tests which
|
|
# read the same input data without needing to re-insert data for every test,
|
|
# so overall the test suite is faster.
|
|
@pytest.fixture(scope="module")
|
|
def test_table_sn_with_sorted_partition(test_table_sn):
|
|
p = random_string()
|
|
items = [{'p': p, 'c': i, 'a': random_string()} for i in range(12)]
|
|
with test_table_sn.batch_writer() as batch:
|
|
for item in items:
|
|
batch.put_item(item)
|
|
# Add another partition just to make sure that a query of just
|
|
# partition p can't just match the entire table and still succeed
|
|
batch.put_item({'p': random_string(), 'c': 123, 'a': random_string()})
|
|
yield test_table_sn, p, items
|
|
|
|
@pytest.fixture(scope="module")
|
|
def test_table_ss_with_sorted_partition(test_table_ss):
|
|
p = random_string()
|
|
items = [{'p': p, 'c': str(i).zfill(3), 'a': random_string()} for i in range(12)]
|
|
with test_table_ss.batch_writer() as batch:
|
|
for item in items:
|
|
batch.put_item(item)
|
|
batch.put_item({'p': random_string(), 'c': '123', 'a': random_string()})
|
|
yield test_table_ss, p, items
|
|
|
|
@pytest.fixture(scope="module")
|
|
def test_table_sb_with_sorted_partition(test_table_sb):
|
|
p = random_string()
|
|
items = [{'p': p, 'c': bytearray(str(i).zfill(3), 'ascii'), 'a': random_string()} for i in range(12)]
|
|
with test_table_sb.batch_writer() as batch:
|
|
for item in items:
|
|
batch.put_item(item)
|
|
batch.put_item({'p': random_string(), 'c': bytearray('123', 'ascii'), 'a': random_string()})
|
|
yield test_table_sb, p, items
|
|
|
|
|
|
# A key condition with just a partition key, returning the entire partition
|
|
def test_key_condition_expression_partition(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p', ExpressionAttributeValues={':p': p})
|
|
# Both items and got_items are already sorted, so simple equality check is enough
|
|
assert(got_items == items)
|
|
|
|
# As usual, whitespace in expression is ignored
|
|
def test_key_condition_expression_whitespace(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression=' p = :p ', ExpressionAttributeValues={':p': p})
|
|
# Both items and got_items are already sorted, so simple equality check is enough
|
|
assert(got_items == items)
|
|
|
|
# The KeyConditionExpression may only have one condition in each key column.
|
|
# A condition of "p=:p AND p=:p", while logically equivalent to just "p=:p",
|
|
# is not allowed.
|
|
def test_key_condition_expression_and_same(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*one'):
|
|
full_query(table, KeyConditionExpression='p=:p AND p=:p', ExpressionAttributeValues={':p': p})
|
|
|
|
# As usual, if we have an expression referring to a missing attribute value,
|
|
# we get an error:
|
|
def test_key_condition_expression_partition_missing_val(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*:p'):
|
|
full_query(table, KeyConditionExpression='p=:p')
|
|
|
|
# As usual, if we have an expression with unused values, we get an error:
|
|
def test_key_condition_expression_partition_unused_val(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*qq'):
|
|
full_query(table, KeyConditionExpression='p=:p', ExpressionAttributeValues={':p': p, ':qq': 3})
|
|
|
|
# The condition of the partition key must be an equality. Test that other
|
|
# operators such as "<" are not supported. They are not supported because
|
|
# partition keys are not sorted in the database, so there is no efficient
|
|
# way to implement such a query (and, anyway, it would be a Scan, not a
|
|
# Query).
|
|
def test_key_condition_expression_partition_eq_only(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*supported'):
|
|
full_query(table, KeyConditionExpression='p<:p', ExpressionAttributeValues={':p': p})
|
|
|
|
# The KeyConditionExpression must define the partition key p, it can't be
|
|
# just on the sort key c.
|
|
def test_key_condition_expression_no_partition(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.* p'):
|
|
full_query(table, KeyConditionExpression='c=:c', ExpressionAttributeValues={':c': 3})
|
|
|
|
# The following tests test the various condition operators on the sort key,
|
|
# for a *numeric* sort key:
|
|
|
|
# Test the "=" operator on a numeric sort key:
|
|
def test_key_condition_expression_num_eq(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
expected_items = [item for item in items if item['c'] == 3]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "<" operator on a numeric sort key:
|
|
def test_key_condition_expression_num_lt(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c<:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
expected_items = [item for item in items if item['c'] < 5]
|
|
assert(got_items == expected_items)
|
|
|
|
# The attribute name does not necessarily need to appear on the left side
|
|
# of the operator, it can also appear on the right side! Note how for
|
|
# "<", reversing the order obviously reverses the operator in the condition.
|
|
def test_key_condition_expression_num_eq_reverse(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression=':p=p AND :c=c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
expected_items = [item for item in items if item['c'] == 5]
|
|
assert(got_items == expected_items)
|
|
|
|
def test_key_condition_expression_num_lt_reverse(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression=':p=p AND :c<c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
expected_items = [item for item in items if item['c'] > 5]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "<=" operator on a numeric sort key:
|
|
def test_key_condition_expression_num_le(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c<=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
expected_items = [item for item in items if item['c'] <= 5]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the ">" operator on a numeric sort key:
|
|
def test_key_condition_expression_num_gt(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c>:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
expected_items = [item for item in items if item['c'] > 5]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the ">=" operator on a numeric sort key:
|
|
def test_key_condition_expression_num_ge(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c>=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
expected_items = [item for item in items if item['c'] >= 5]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "BETWEEN/AND" ternary operator on a numeric sort key:
|
|
def test_key_condition_expression_num_between(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c BETWEEN :c1 AND :c2',
|
|
ExpressionAttributeValues={':p': p, ':c1': 4, ':c2': 7})
|
|
expected_items = [item for item in items if item['c'] >= 4 and item['c'] <= 7]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test that the BETWEEN and AND keywords (with its two different meanings!)
|
|
# are case-insensitive.
|
|
def test_key_condition_expression_case_insensitive(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AnD c BeTwEeN :c1 aNd :c2',
|
|
ExpressionAttributeValues={':p': p, ':c1': 4, ':c2': 7})
|
|
expected_items = [item for item in items if item['c'] >= 4 and item['c'] <= 7]
|
|
assert(got_items == expected_items)
|
|
|
|
# The begins_with function does *not* work on a numeric sort key (it only
|
|
# works on strings or bytes - we'll check this later):
|
|
def test_key_condition_expression_num_begins(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*begins_with'):
|
|
full_query(table, KeyConditionExpression='p=:p AND begins_with(c, :c)',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
|
|
# begins_with() is the only supported function in KeyConditionExpression.
|
|
# Unknown functions like dog() or functions supported in other expression
|
|
# types but not here (like attribute_exists()) result in errors.
|
|
def test_key_condition_expression_unknown(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*dog'):
|
|
full_query(table, KeyConditionExpression='p=:p AND dog(c, :c)',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
with pytest.raises(ClientError, match='ValidationException.*attribute_exists'):
|
|
full_query(table, KeyConditionExpression='p=:p AND attribute_exists(c)',
|
|
ExpressionAttributeValues={':p': p})
|
|
|
|
# As we already tested above for the partition key, it is not allowed to
|
|
# have multiple conditions on the same key column - here we test this for the
|
|
# sort key. This is why the separate BETWEEN operator exists...
|
|
def test_key_condition_expression_multi(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c>=:c1 AND c>=:c1',
|
|
ExpressionAttributeValues={':p': p, ':c1': 3})
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c>=:c1 AND c<=:c2',
|
|
ExpressionAttributeValues={':p': p, ':c1': 3, ':c2': 7})
|
|
|
|
# Although the syntax for KeyConditionExpression is only a subset of that
|
|
# of ConditionExpression, it turns out that DynamoDB actually use the same
|
|
# parser for both. The implications of this for KeyConditionExpression
|
|
# parsing are interesting. In some cases, we simply see different error
|
|
# messages - e.g., unsupported operators (IN and <>) are not considered
|
|
# syntax errors but generate a slightly different error. The same thing
|
|
# is true for the OR and NOT keywords, which generate special errors.
|
|
# A slightly more interesting result of reusing the ConditionExpression
|
|
# parser is that parentheses, although they are quite useless for
|
|
# KeyConditionExpression (when there is always just one or two conditions
|
|
# ANDed together), *are* allowed in KeyConditionExpression.
|
|
def test_key_condition_expression_parser(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
# The operators "<>" and "IN" are parsed, but not allowed:
|
|
with pytest.raises(ClientError, match='ValidationException.*operator'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c<>:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
with pytest.raises(ClientError, match='ValidationException.*operator'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c IN (:c)',
|
|
ExpressionAttributeValues={':p': p, ':c': 5})
|
|
# The "OR" or "NOT" operators are parsed, but not allowed:
|
|
with pytest.raises(ClientError, match='ValidationException.*OR'):
|
|
full_query(table, KeyConditionExpression='c=:c OR p=:p',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
with pytest.raises(ClientError, match='ValidationException.*NOT'):
|
|
full_query(table, KeyConditionExpression='NOT c=:c AND p=:p',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
# Unnecessary parentheses are allowed around the entire expression,
|
|
# and on each primitive condition in it:
|
|
got_items = full_query(table, KeyConditionExpression='((c=:c) AND (p=:p))',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
expected_items = [item for item in items if item['c'] == 3]
|
|
assert(got_items == expected_items)
|
|
# Strangely, although one pair of unnecessary parentheses are allowed
|
|
# in each level, DynamoDB forbids more than one - it refuses to accept
|
|
# the expression ((c=:c) AND ((p=:p))) with one too many redundant levels
|
|
# of parentheses. However, we chose not to implement this extra check
|
|
# in Alternator, so the following test is commented out:
|
|
#with pytest.raises(ClientError, match='ValidationException'):
|
|
# full_query(table, KeyConditionExpression='((c=:c) AND ((p=:p)))',
|
|
# ExpressionAttributeValues={':p': p, ':c': 3})
|
|
|
|
# A simple case of syntax error - unknown operator "!=".
|
|
def test_key_condition_expression_syntax(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*yntax'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c!=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
|
|
# Unsurprisingly, an empty KeyConditionExpression is an error
|
|
def test_key_condition_expression_empty(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*empty'):
|
|
full_query(table, KeyConditionExpression='')
|
|
|
|
# It is not allowed to use a non-key column in KeyConditionExpression
|
|
def test_key_condition_expression_non_key(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
# The specific errors returned by DynamoDB in this case are somewhat
|
|
# peculiar: If we have three conditions - on both key columns and on
|
|
# a non-key columns - the error is that only two are allowed.
|
|
# But if we just have two conditions, the error says that it expects
|
|
# the second condition to be on the schema's sort key.
|
|
# Alternator's parser doesn't need to give exactly these peculiar
|
|
# errors, though.
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c<:c AND a<:a',
|
|
ExpressionAttributeValues={':p': p, ':c': 3, ':a': 3})
|
|
with pytest.raises(ClientError, match='ValidationException.*key'):
|
|
full_query(table, KeyConditionExpression='p=:p AND a<:a',
|
|
ExpressionAttributeValues={':p': p, ':a': 3})
|
|
# DynamoDB's parser also, as usual, understands nested attribute names
|
|
# in expressions, but doesn't allow them in condition expressions.
|
|
with pytest.raises(ClientError, match='ValidationException.*nested'):
|
|
full_query(table, KeyConditionExpression='p=:p AND a.b<:a',
|
|
ExpressionAttributeValues={':p': p, ':a': 3})
|
|
|
|
# A condition needs to compare a column to a constant - we can't compare
|
|
# two columns, nor two constants
|
|
def test_key_condition_expression_bad_compare(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression='p=c')
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression='p=a')
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression=':p=:a',
|
|
ExpressionAttributeValues={':p': p, ':a': 3})
|
|
|
|
# Nor can a "condition" be just a single key or constant:
|
|
def test_key_condition_expression_bad_value(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression='p')
|
|
with pytest.raises(ClientError, match='ValidationException'):
|
|
full_query(table, KeyConditionExpression=':a',
|
|
ExpressionAttributeValues={':a': 3})
|
|
|
|
# In all tests above the condition had p first, c second. Verify that this
|
|
# order can be reversed.
|
|
def test_key_condition_expression_order(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='c=:c AND p=:p',
|
|
ExpressionAttributeValues={':p': p, ':c': 3})
|
|
expected_items = [item for item in items if item['c'] == 3]
|
|
assert(got_items == expected_items)
|
|
|
|
# All tests above had the attribute names written explicitly in the expression.
|
|
# Try the same with attribute name references (#something):
|
|
def test_key_condition_expression_name_ref(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='#name1=:p AND #name2=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 3},
|
|
ExpressionAttributeNames={'#name1': 'p', '#name2': 'c'})
|
|
expected_items = [item for item in items if item['c'] == 3]
|
|
assert(got_items == expected_items)
|
|
|
|
# Missing or unused attribute name references cause errors:
|
|
def test_key_condition_expression_name_ref_error(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*name2'):
|
|
full_query(table, KeyConditionExpression='#name1=:p AND #name2=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': 3},
|
|
ExpressionAttributeNames={'#name1': 'p'})
|
|
with pytest.raises(ClientError, match='ValidationException.*name2'):
|
|
full_query(table, KeyConditionExpression='#name1=:p',
|
|
ExpressionAttributeValues={':p': p},
|
|
ExpressionAttributeNames={'#name1': 'p', '#name2': 'c'})
|
|
|
|
# The condition knows the key attributes' types from the schema, and should
|
|
# refuse to compare one to a value with the wrong type, resulting in a
|
|
# ValidationException
|
|
def test_key_condition_expression_wrong_type(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*type'):
|
|
full_query(table, KeyConditionExpression='p=:num',
|
|
ExpressionAttributeValues={':num': 3})
|
|
with pytest.raises(ClientError, match='ValidationException.*type'):
|
|
full_query(table, KeyConditionExpression='p=:p AND c=:str',
|
|
ExpressionAttributeValues={':p': p, ':str': 'hello'})
|
|
|
|
# The following tests test the various condition operators on the sort key,
|
|
# for a *string* sort key:
|
|
|
|
# Test the "=" operator on a string sort key:
|
|
def test_key_condition_expression_str_eq(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': '003'})
|
|
expected_items = [item for item in items if item['c'] == '003']
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "<" operator on a string sort key:
|
|
def test_key_condition_expression_str_lt(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c<:c',
|
|
ExpressionAttributeValues={':p': p, ':c': '005'})
|
|
expected_items = [item for item in items if item['c'] < '005']
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "<=" operator on a string sort key:
|
|
def test_key_condition_expression_str_le(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c<=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': '005'})
|
|
expected_items = [item for item in items if item['c'] <= '005']
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the ">" operator on a string sort key:
|
|
def test_key_condition_expression_str_gt(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c>:c',
|
|
ExpressionAttributeValues={':p': p, ':c': '005'})
|
|
expected_items = [item for item in items if item['c'] > '005']
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the ">=" operator on a string sort key:
|
|
def test_key_condition_expression_str_ge(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c>=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': '005'})
|
|
expected_items = [item for item in items if item['c'] >= '005']
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "BETWEEN/AND" ternary operator on a string sort key:
|
|
def test_key_condition_expression_str_between(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c BETWEEN :c1 AND :c2',
|
|
ExpressionAttributeValues={':p': p, ':c1': '004', ':c2': '007'})
|
|
expected_items = [item for item in items if item['c'] >= '004' and item['c'] <= '007']
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the begins_with function on a string sort key:
|
|
def test_key_condition_expression_str_begins(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND begins_with(c,:c)',
|
|
ExpressionAttributeValues={':p': p, ':c': '00'})
|
|
expected_items = [item for item in items if item['c'].startswith('00')]
|
|
assert(got_items == expected_items)
|
|
|
|
# The function name begins_with is case-sensitive - it doesn't work with
|
|
# other capitalization. Note that above we already tested the general case
|
|
# of an unknown function (e.g., dog) - this is really just another example.
|
|
def test_key_condition_expression_str_begins_case(test_table_ss_with_sorted_partition):
|
|
table, p, items = test_table_ss_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*BEGINS_WITH'):
|
|
full_query(table, KeyConditionExpression='p=:p AND BEGINS_WITH(c,:c)',
|
|
ExpressionAttributeValues={':p': p, ':c': '00'})
|
|
|
|
# The following tests test the various condition operators on the sort key,
|
|
# for a *bytes* sort key:
|
|
|
|
# Test the "=" operator on a bytes sort key:
|
|
def test_key_condition_expression_bytes_eq(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray('003', 'ascii')})
|
|
expected_items = [item for item in items if item['c'] == bytearray('003', 'ascii')]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "<" operator on a bytes sort key:
|
|
def test_key_condition_expression_bytes_lt(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c<:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray('005', 'ascii')})
|
|
expected_items = [item for item in items if item['c'] < bytearray('005', 'ascii')]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "<=" operator on a bytes sort key:
|
|
def test_key_condition_expression_bytes_le(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c<=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray('005', 'ascii')})
|
|
expected_items = [item for item in items if item['c'] <= bytearray('005', 'ascii')]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the ">" operator on a bytes sort key:
|
|
def test_key_condition_expression_bytes_gt(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c>:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray('005', 'ascii')})
|
|
expected_items = [item for item in items if item['c'] > bytearray('005', 'ascii')]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the ">=" operator on a string sort key:
|
|
def test_key_condition_expression_bytes_ge(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c>=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray('005', 'ascii')})
|
|
expected_items = [item for item in items if item['c'] >= bytearray('005', 'ascii')]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the "BETWEEN/AND" ternary operator on a bytes sort key:
|
|
def test_key_condition_expression_bytes_between(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND c BETWEEN :c1 AND :c2',
|
|
ExpressionAttributeValues={':p': p, ':c1': bytearray('004', 'ascii'), ':c2': bytearray('007', 'ascii')})
|
|
expected_items = [item for item in items if item['c'] >= bytearray('004', 'ascii') and item['c'] <= bytearray('007', 'ascii')]
|
|
assert(got_items == expected_items)
|
|
|
|
# Test the begins_with function on a bytes sort key:
|
|
def test_key_condition_expression_bytes_begins(test_table_sb_with_sorted_partition):
|
|
table, p, items = test_table_sb_with_sorted_partition
|
|
got_items = full_query(table, KeyConditionExpression='p=:p AND begins_with(c,:c)',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray('00', 'ascii')})
|
|
expected_items = [item for item in items if item['c'].startswith(bytearray('00', 'ascii'))]
|
|
assert(got_items == expected_items)
|
|
|
|
# Query and KeyConditionExpression works also on a table with just a partition
|
|
# key and no sort key, although obviously it isn't very useful (for such
|
|
# tables, Query is just an elaborate way to do a GetItem).
|
|
def test_key_condition_expression_hash_only(test_table_s):
|
|
p = random_string()
|
|
item = {'p': p, 'val': 'hello'}
|
|
test_table_s.put_item(Item=item)
|
|
got_items = full_query(test_table_s, KeyConditionExpression='p=:p', ExpressionAttributeValues={':p': p})
|
|
assert(got_items == [item])
|
|
|
|
# A Query cannot specify both KeyConditions and KeyConditionExpression
|
|
def test_key_condition_expression_and_conditions(test_table_sn_with_sorted_partition):
|
|
table, p, items = test_table_sn_with_sorted_partition
|
|
with pytest.raises(ClientError, match='ValidationException.*both'):
|
|
full_query(table,
|
|
KeyConditionExpression='p=:p',
|
|
ExpressionAttributeValues={':p': p},
|
|
KeyConditions={'c' : {'AttributeValueList': [3],
|
|
'ComparisonOperator': 'GT'}}
|
|
)
|
|
|
|
# Demonstrate that issue #6573 was not a bug for KeyConditionExpression:
|
|
# binary strings are ordered as unsigned bytes, i.e., byte 128 comes after
|
|
# 127, not as signed bytes.
|
|
# Test the five ordering operators: <, <=, >, >=, between
|
|
def test_key_condition_expression_unsigned_bytes(test_table_sb):
|
|
p = random_string()
|
|
items = [{'p': p, 'c': bytearray([i])} for i in range(126,129)]
|
|
with test_table_sb.batch_writer() as batch:
|
|
for item in items:
|
|
batch.put_item(item)
|
|
got_items = full_query(test_table_sb,
|
|
KeyConditionExpression='p=:p AND c<:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray([127])})
|
|
expected_items = [item for item in items if item['c'] < bytearray([127])]
|
|
assert(got_items == expected_items)
|
|
got_items = full_query(test_table_sb,
|
|
KeyConditionExpression='p=:p AND c<=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray([127])})
|
|
expected_items = [item for item in items if item['c'] <= bytearray([127])]
|
|
assert(got_items == expected_items)
|
|
got_items = full_query(test_table_sb,
|
|
KeyConditionExpression='p=:p AND c>:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray([127])})
|
|
expected_items = [item for item in items if item['c'] > bytearray([127])]
|
|
assert(got_items == expected_items)
|
|
got_items = full_query(test_table_sb,
|
|
KeyConditionExpression='p=:p AND c>=:c',
|
|
ExpressionAttributeValues={':p': p, ':c': bytearray([127])})
|
|
expected_items = [item for item in items if item['c'] >= bytearray([127])]
|
|
assert(got_items == expected_items)
|
|
got_items = full_query(test_table_sb,
|
|
KeyConditionExpression='p=:p AND c BETWEEN :c1 AND :c2',
|
|
ExpressionAttributeValues={':p': p, ':c1': bytearray([127]), ':c2': bytearray([128])})
|
|
expected_items = [item for item in items if item['c'] >= bytearray([127]) and item['c'] <= bytearray([128])]
|
|
assert(got_items == expected_items)
|
|
|
|
# The following is an older test we had, which test one arbitrary use case
|
|
# for KeyConditionExpression. It uses filled_test_table (the one we also
|
|
# use in test_scan.py) instead of the fixtures defined in this file.
|
|
# The only interesting thing about this test is that instead of creating
|
|
# the KeyConditionExpression string, it uses boto3's "Key" condition builder.
|
|
# That shouldn't make any difference, however - that builder also builds a
|
|
# string.
|
|
def test_query_key_condition_expression(dynamodb, filled_test_table):
|
|
test_table, items = filled_test_table
|
|
paginator = dynamodb.meta.client.get_paginator('query')
|
|
got_items = []
|
|
for page in paginator.paginate(TableName=test_table.name, KeyConditionExpression=Key("p").eq("long") & Key("c").lt("12")):
|
|
got_items += page['Items']
|
|
assert multiset([item for item in items if item['p'] == 'long' and item['c'] < '12']) == multiset(got_items)
|