We already have a test, test_limits.py::test_deeply_nested_expression_2,
which checks that in the long condition expression
a<b or (a<b or (a<b or (a<b or (....))))
with more than MAX_DEPTH (=400) repeats is rejected by Alternator,
as part of commit 04e5082d52 which
restricted the depth of the recursive parser to prevent crashing Scylla.
However, I got curious what will happen without the parentheses:
a<b or a<b or a<b or a<b or ...
It turns out that our parser actually parses this syntax without
recursion - it's just a loop (a "*" in the Antlr alternator/expressions.g
allows reading more and more ORs in a loop). So Alternator doesn't limit
the length of this expression more than the length limit of 4096 bytes
which we also have. We can fit 584 repeats in the above expression in
4096 bytes, and it will not be rejected even though 584 > 400.
This test confirms that this is indeed the case.
The test is Scylla-only because on DynamoDB, this expression is rejected
because it has more than 300 "OR" operators. Scylla doesn't have this
specific limit - we believe the other limitations (on total expression
length, and on depth) are better for protecting Scylla. Remember that
in an expression like "(((((((((((((" there is a very high recursion
depth of the parser but zero operators, so counting the operators does
nothing to protect Scylla.
Signed-off-by: Nadav Har'El <nyh@scylladb.com>
Closes scylladb/scylladb#23973
975 lines
53 KiB
Python
975 lines
53 KiB
Python
# Copyright 2021-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
|
|
#############################################################################
|
|
# Tests for various limits, which did not fit naturally into other test files
|
|
#############################################################################
|
|
|
|
import pytest
|
|
from botocore.exceptions import ClientError
|
|
|
|
from test.alternator.util import random_string, new_test_table, full_query
|
|
from test.alternator.test_gsi import assert_index_query
|
|
|
|
|
|
#############################################################################
|
|
# The following tests check the limits on attribute name lengths.
|
|
# According to the DynamoDB documentation, attribute names are usually
|
|
# limited to 64K bytes, and the only exceptions are:
|
|
# 1. Secondary index partition/sort key names are limited to 255 characters.
|
|
# 2. In LSI, attributes listed for projection.
|
|
# We'll test all these cases below in several separate tests.
|
|
# We found a additional exceptions - the base-table key names are also limited
|
|
# to 255 bytes, and the expiration-time column given to UpdateTimeToLive is
|
|
# also limited to 255 character. We test the last fact in a different test
|
|
# file: test_ttl.py::test_update_ttl_errors.
|
|
|
|
# Attribute length test 1: non-key attribute names below 64KB are usable in
|
|
# PutItem, UpdateItem, GetItem, and also in various expressions (condition,
|
|
# update and projection) and their archaic pre-expression alternatives.
|
|
def test_limit_attribute_length_nonkey_good(test_table_s):
|
|
p = random_string()
|
|
too_long_name = random_string(64)*1024
|
|
long_name = too_long_name[:-1]
|
|
# Try legal long_name:
|
|
test_table_s.put_item(Item={'p': p, long_name: 1, 'another': 2 })
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, long_name: 1, 'another': 2 }
|
|
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True,
|
|
ProjectionExpression='#name', ExpressionAttributeNames={'#name': long_name})['Item'] == {long_name: 1}
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True,
|
|
AttributesToGet=[long_name])['Item'] == {long_name: 1}
|
|
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={long_name: {'Value': 2, 'Action': 'PUT'}})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, long_name: 2, 'another': 2 }
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #name = :val',
|
|
ExpressionAttributeNames={'#name': long_name},
|
|
ExpressionAttributeValues={':val': 3})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, long_name: 3, 'another': 2 }
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #name = #name+:val',
|
|
ExpressionAttributeNames={'#name': long_name},
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, long_name: 4, 'another': 2 }
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #name = #name+:val',
|
|
ConditionExpression='#name = :oldval',
|
|
ExpressionAttributeNames={'#name': long_name},
|
|
ExpressionAttributeValues={':val': 1, ':oldval': 4})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, long_name: 5, 'another': 2 }
|
|
|
|
# Attribute length test 2: attribute names 64KB or above generate an error
|
|
# in the aforementioned cases. Note that contrary to what the DynamoDB
|
|
# documentation suggests, the length 64KB itself is not allowed - 65535
|
|
# (which we tested above) is the last accepted size.
|
|
# Reproduces issue #9169.
|
|
def test_limit_attribute_length_nonkey_bad(test_table_s):
|
|
p = random_string()
|
|
too_long_name = random_string(64)*1024
|
|
with pytest.raises(ClientError, match='ValidationException.*6553[56]'):
|
|
test_table_s.put_item(Item={'p': p, too_long_name: 1})
|
|
with pytest.raises(ClientError, match='ValidationException.*6553[56]'):
|
|
test_table_s.get_item(Key={'p': p}, ProjectionExpression='#name',
|
|
ExpressionAttributeNames={'#name': too_long_name})
|
|
with pytest.raises(ClientError, match='ValidationException.*6553[56]'):
|
|
test_table_s.get_item(Key={'p': p}, AttributesToGet=[too_long_name])
|
|
with pytest.raises(ClientError, match='ValidationException.*6553[56]'):
|
|
test_table_s.update_item(Key={'p': p}, AttributeUpdates={too_long_name: {'Value': 2, 'Action': 'PUT'}})
|
|
with pytest.raises(ClientError, match='ValidationException.*6553[56]'):
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET #name = :val',
|
|
ExpressionAttributeNames={'#name': too_long_name},
|
|
ExpressionAttributeValues={':val': 3})
|
|
with pytest.raises(ClientError, match='ValidationException.*6553[56]'):
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :val',
|
|
ConditionExpression='#name = :val',
|
|
ExpressionAttributeNames={'#name': too_long_name},
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# Attribute length test 3: Test that *key* (hash and range) attribute names
|
|
# up to 255 characters are allowed. In the test below we'll see that larger
|
|
# sizes aren't allowed.
|
|
def test_limit_attribute_length_key_good(dynamodb):
|
|
long_name1 = random_string(255)
|
|
long_name2 = random_string(255)
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': long_name1, 'KeyType': 'HASH' },
|
|
{ 'AttributeName': long_name2, 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': long_name1, 'AttributeType': 'S' },
|
|
{ 'AttributeName': long_name2, 'AttributeType': 'S' }]) as table:
|
|
table.put_item(Item={long_name1: 'hi', long_name2: 'ho', 'another': 2 })
|
|
assert table.get_item(Key={long_name1: 'hi', long_name2: 'ho'}, ConsistentRead=True)['Item'] == {long_name1: 'hi', long_name2: 'ho', 'another': 2 }
|
|
|
|
# Attribute length test 4: Test that *key* attribute names more than 255
|
|
# characters are not allowed - not for hash key and not for range key.
|
|
# Strangely, this limitation is not explicitly mentioned in the DynamoDB
|
|
# documentation - which only mentions that SI keys are limited to 255 bytes,
|
|
# but forgets to mention base-table keys.
|
|
# Reproduces issue #9169.
|
|
def test_limit_attribute_length_key_bad(dynamodb):
|
|
too_long_name = random_string(256)
|
|
|
|
# Test with HASH-type or non-RANGE-type key name too long
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': too_long_name, 'KeyType': 'HASH' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': too_long_name, 'AttributeType': 'S' } ]) as table:
|
|
pass
|
|
|
|
# Test with RANGE-type (or non-HASH-type key) name too long
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'x', 'KeyType': 'HASH', },
|
|
{ 'AttributeName': too_long_name, 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': too_long_name, 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'x', 'AttributeType': 'S' } ]) as table:
|
|
pass
|
|
|
|
# Test that *key* attribute names more than 255 characters are not allowed,
|
|
# not for hash key and not for range key. Strangely, this limitation is not
|
|
# explicitly mentioned in the DynamoDB documentation.
|
|
# The tests here cover the cases of too long names in incorrect (incoherent)
|
|
# user requests. The incoherence here means that AttributeDefinitions should
|
|
# refer to AttributeName value that is used inside KeySchema as well.
|
|
# In all the cases, DynamoDB returns attrribute name length-related errors,
|
|
# ignoring the issues with the consistency.
|
|
# Reproduces issue #9169.
|
|
def test_limit_attribute_length_key_bad_incoherent_names(dynamodb):
|
|
too_long_name = random_string(256)
|
|
|
|
# Tests with HASH-type or non-RANGE-type key name too long
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': too_long_name, 'KeyType': 'HASH' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': "incoherent-short-name", 'AttributeType': 'S' } ]) as table:
|
|
pass
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': "incoherent-short-name", 'KeyType': 'HASH' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': too_long_name, 'AttributeType': 'S' } ]) as table:
|
|
pass
|
|
|
|
# Tests with RANGE-type (or non-HASH-type key) name too long
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'x', 'KeyType': 'HASH', },
|
|
{ 'AttributeName': too_long_name, 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': "incoherent-short-name", 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'x', 'AttributeType': 'S' } ]) as table:
|
|
pass
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'x', 'KeyType': 'HASH', },
|
|
{ 'AttributeName': "incoherent-short-name", 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': too_long_name, 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'x', 'AttributeType': 'S' } ]) as table:
|
|
pass
|
|
|
|
# Attribute length tests 5,6: similar as the above tests for the 255-byte
|
|
# limit for base table length, here we check that the same limit also applies
|
|
# to key columns in GSI and LSI.
|
|
def test_limit_attribute_length_gsi_lsi_good(dynamodb):
|
|
long_name1 = random_string(255)
|
|
long_name2 = random_string(255)
|
|
long_name3 = random_string(255)
|
|
long_name4 = random_string(255)
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': long_name1, 'KeyType': 'HASH' },
|
|
{ 'AttributeName': long_name2, 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': long_name1, 'AttributeType': 'S' },
|
|
{ 'AttributeName': long_name2, 'AttributeType': 'S' },
|
|
{ 'AttributeName': long_name3, 'AttributeType': 'S' },
|
|
{ 'AttributeName': long_name4, 'AttributeType': 'S' }],
|
|
GlobalSecondaryIndexes=[
|
|
{ 'IndexName': 'gsi', 'KeySchema': [
|
|
{ 'AttributeName': long_name3, 'KeyType': 'HASH' },
|
|
{ 'AttributeName': long_name4, 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': long_name1, 'KeyType': 'HASH' },
|
|
{ 'AttributeName': long_name4, 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
table.put_item(Item={long_name1: 'hi', long_name2: 'ho', long_name3: 'dog', long_name4: 'cat' })
|
|
assert table.get_item(Key={long_name1: 'hi', long_name2: 'ho'}, ConsistentRead=True)['Item'] == {long_name1: 'hi', long_name2: 'ho', long_name3: 'dog', long_name4: 'cat' }
|
|
# Verify the content through the indexes. LSI can use ConsistentRead
|
|
# but GSI might need to retry to find the content:
|
|
assert full_query(table, IndexName='lsi', ConsistentRead=True,
|
|
KeyConditions={
|
|
long_name1: {'AttributeValueList': ['hi'], 'ComparisonOperator': 'EQ'},
|
|
long_name4: {'AttributeValueList': ['cat'], 'ComparisonOperator': 'EQ'},
|
|
}) == [{long_name1: 'hi', long_name2: 'ho', long_name3: 'dog', long_name4: 'cat'}]
|
|
assert_index_query(table, 'gsi',
|
|
[{long_name1: 'hi', long_name2: 'ho', long_name3: 'dog', long_name4: 'cat'}],
|
|
KeyConditions={
|
|
long_name3: {'AttributeValueList': ['dog'], 'ComparisonOperator': 'EQ'},
|
|
long_name4: {'AttributeValueList': ['cat'], 'ComparisonOperator': 'EQ'},
|
|
})
|
|
|
|
# Reproduces issue #9169.
|
|
def test_limit_attribute_length_gsi_lsi_bad(dynamodb):
|
|
too_long_name = random_string(256)
|
|
|
|
# GSI attr. name length test
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': too_long_name, 'AttributeType': 'S' } ],
|
|
GlobalSecondaryIndexes=[
|
|
{ 'IndexName': 'gsi', 'KeySchema': [
|
|
{ 'AttributeName': too_long_name, 'KeyType': 'HASH' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
pass
|
|
|
|
# LSI attr. name length test
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': too_long_name, 'AttributeType': 'S' } ],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': too_long_name, 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
pass
|
|
|
|
# The tests here cover the cases of too long names in incorrect (incoherent)
|
|
# user requests. The incoherence here means:
|
|
# 1. GlobalSecondaryIndexes should refer to AttributeName value that is used
|
|
# inside AttributeDefinitions as well.
|
|
# 2. LocalSecondaryIndexes should refer to AttributeName value that is used
|
|
# inside AttributeDefinitions as well.
|
|
# In all the cases, DynamoDB returns attribute name length-related errors,
|
|
# ignoring the issues with the consistency.
|
|
# Reproduces issue #9169.
|
|
def test_limit_attribute_length_gsi_lsi_bad_incoherent_names(dynamodb):
|
|
too_long_name = random_string(256)
|
|
|
|
# GSI attr. name length tests
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': too_long_name, 'AttributeType': 'S' } ],
|
|
GlobalSecondaryIndexes=[
|
|
{ 'IndexName': 'gsi', 'KeySchema': [
|
|
{ 'AttributeName': "incoherent-short-name", 'KeyType': 'HASH' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
pass
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': "incoherent-short-name", 'AttributeType': 'S' } ],
|
|
GlobalSecondaryIndexes=[
|
|
{ 'IndexName': 'gsi', 'KeySchema': [
|
|
{ 'AttributeName': too_long_name, 'KeyType': 'HASH' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
pass
|
|
|
|
# LSI attr. name length tests
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': too_long_name, 'AttributeType': 'S' } ],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': "incoherent-short-name", 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
pass
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': "incoherent-short-name", 'AttributeType': 'S' } ],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': too_long_name, 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
pass
|
|
|
|
# Attribute length tests 7,8: In an LSI, projected attribute names are also
|
|
# limited to 255 bytes. This is explicitly mentioned in the DynamoDB
|
|
# documentation. For GSI this is also true (but not explicitly mentioned).
|
|
# This limitation is only true to attributes *explicitly* projected by name -
|
|
# attributes projected as part as ALL can be bigger (up to the usual 64KB
|
|
# limit).
|
|
# Reproduces issue #9169.
|
|
@pytest.mark.xfail(reason="issue #5036: projection in GSI and LSI not supported")
|
|
def test_limit_attribute_length_gsi_lsi_projection_bad(dynamodb):
|
|
too_long_name = random_string(256)
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' } ],
|
|
GlobalSecondaryIndexes=[
|
|
{ 'IndexName': 'gsi', 'KeySchema': [
|
|
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
|
], 'Projection': { 'ProjectionType': 'INCLUDE',
|
|
'NonKeyAttributes': [too_long_name]}
|
|
}
|
|
]) as table:
|
|
pass
|
|
with pytest.raises(ClientError, match='ValidationException.*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' } ],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' } ],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'c', 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'INCLUDE',
|
|
'NonKeyAttributes': [too_long_name]}
|
|
}
|
|
]) as table:
|
|
pass
|
|
|
|
# Above we tested asking to project a specific column which has very long
|
|
# name, and failed the table creation. Here we show that a GSI/LSI which
|
|
# projects ALL, and has some attribute names with >255 but lower than the
|
|
# normal attribute name limit of 64KB, gets projected fine.
|
|
def test_limit_attribute_length_gsi_lsi_projection_all(dynamodb):
|
|
too_long_name = random_string(256)
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' }
|
|
],
|
|
GlobalSecondaryIndexes=[
|
|
{ 'IndexName': 'gsi', 'KeySchema': [
|
|
{ 'AttributeName': 'c', 'KeyType': 'HASH' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
},
|
|
],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'c', 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
# As we tested above, there is no problem adding a non-key attribute
|
|
# which has a >255 byte name. This is true even if this attribute is
|
|
# implicitly copied to the GSI or LSI by the ProjectionType=ALL.
|
|
table.put_item(Item={'a': 'hi', 'b': 'ho', 'c': 'dog', too_long_name: 'cat' })
|
|
assert table.get_item(Key={'a': 'hi', 'b': 'ho'}, ConsistentRead=True)['Item'] == {'a': 'hi', 'b': 'ho', 'c': 'dog', too_long_name: 'cat' }
|
|
# GSI cannot use ConsistentRead so we may need to retry the read, so
|
|
# we reuse a function that does this
|
|
assert_index_query(table, 'gsi',
|
|
[{'a': 'hi', 'b': 'ho', 'c': 'dog', too_long_name: 'cat'}],
|
|
KeyConditions={'c': {'AttributeValueList': ['dog'],
|
|
'ComparisonOperator': 'EQ'}})
|
|
# LSI can use ConsistentRead:
|
|
assert full_query(table, IndexName='lsi', ConsistentRead=True,
|
|
KeyConditions={
|
|
'a': {'AttributeValueList': ['hi'], 'ComparisonOperator': 'EQ'},
|
|
'c': {'AttributeValueList': ['dog'], 'ComparisonOperator': 'EQ'},
|
|
}) == [{'a': 'hi', 'b': 'ho', 'c': 'dog', too_long_name: 'cat'}]
|
|
|
|
#############################################################################
|
|
# The following tests test various limits of expressions
|
|
# (ProjectionExpression, ConditionExpression, UpdateExpression and
|
|
# FilterExpression) as documented in
|
|
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html
|
|
|
|
# The maximum string length of any of the expression parameters is 4 KB.
|
|
# Check that the length 4096 is allowed, 4097 isn't - on all four expression
|
|
# types.
|
|
def test_limit_expression_len(test_table_s):
|
|
p = random_string()
|
|
string4096 = 'x'*4096
|
|
string4097 = 'x'*4097
|
|
# ProjectionExpression:
|
|
test_table_s.get_item(Key={'p': p}, ProjectionExpression=string4096)
|
|
with pytest.raises(ClientError, match='ValidationException.*ProjectionExpression'):
|
|
test_table_s.get_item(Key={'p': p}, ProjectionExpression=string4097)
|
|
# UpdateExpression:
|
|
spaces4085 = ' '*4085
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression=f'SET{spaces4085}a = :val',
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 1}
|
|
with pytest.raises(ClientError, match='ValidationException.*UpdateExpression'):
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression=f'SET {spaces4085}a = :val',
|
|
ExpressionAttributeValues={':val': 1})
|
|
# ConditionExpression:
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :newval',
|
|
ExpressionAttributeValues={':newval': 2, ':oldval': 1},
|
|
ConditionExpression=f'a{spaces4085} = :oldval')
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item'] == {'p': p, 'a': 2}
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p}, UpdateExpression='SET a = :newval',
|
|
ExpressionAttributeValues={':newval': 3, ':oldval': 2},
|
|
ConditionExpression=f'a {spaces4085} = :oldval')
|
|
# FilterExpression:
|
|
assert full_query(test_table_s, ConsistentRead=True,
|
|
KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}},
|
|
FilterExpression=f'a{spaces4085} = :theval',
|
|
ExpressionAttributeValues={':theval': 2}
|
|
) == [{'p': p, 'a': 2}]
|
|
with pytest.raises(ClientError, match='ValidationException.*FilterExpression'):
|
|
full_query(test_table_s, ConsistentRead=True,
|
|
KeyConditions={'p': {'AttributeValueList': [p], 'ComparisonOperator': 'EQ'}},
|
|
FilterExpression=f'a {spaces4085} = :theval',
|
|
ExpressionAttributeValues={':theval': 2})
|
|
|
|
# The previous test (test_limit_expression_len) makes the 4096-byte length
|
|
# limit of expressions appear very benign - so what if we accept a 10,000-byte
|
|
# expression? Issue #14473 shows one potential harm of long expressions:
|
|
# A long expression can also be deeply nested, and recursive algorithms for
|
|
# parsing or handling these expressions can cause Scylla to crash. The
|
|
# following tests test_limit_expression_len_crash*() used to crash Scylla
|
|
# before the expression length limit was enforced (issue #14473).
|
|
# These tests use ConditionExpression to demonstrate the problem.
|
|
def test_limit_expression_len_crash1(test_table_s):
|
|
# a<b and (a<b and (a<b and (a<b and (a<b and (...))))):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 20000
|
|
condition = "a<b " + "and (a<b "*depth +")"*depth
|
|
# For this expression longer than 4096 bytes, DynamoDB produces the
|
|
# error "Invalid ConditionExpression: Expression size has exceeded the
|
|
# maximum allowed size; expression size: 200004". Scylla used to crash
|
|
# here (after very deep recursion) instead of a clean error.
|
|
with pytest.raises(ClientError, match='ValidationException.*expression size'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_limit_expression_len_crash2(test_table_s):
|
|
# (((((((((((((((a<b)))))))))))))))
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 20000
|
|
condition = "("*depth + "a<b" + ")"*depth
|
|
with pytest.raises(ClientError, match='ValidationException.*expression size'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_limit_expression_len_crash3(test_table_s):
|
|
# ((((((((((((((((((((((((((( - a syntax error
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
condition = "("*15000
|
|
# Although this expression is a syntax error, the fact it is too long
|
|
# should be recognized first.
|
|
with pytest.raises(ClientError, match='ValidationException.*expression size'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_limit_expression_len_crash4(test_table_s):
|
|
# not not not not not ... not a<b
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 20000
|
|
condition = "not "*depth + "a<b"
|
|
with pytest.raises(ClientError, match='ValidationException.*expression size'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
def test_limit_expression_len_crash5(test_table_s):
|
|
# a < f(f(f(f(...(b)))))
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 20000
|
|
condition = "a < " + "f("*depth + "b" + ")"*depth
|
|
with pytest.raises(ClientError, match='ValidationException.*expression size'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# The above tests test_limit_expression_len_crash* checked various cases
|
|
# where very long (>4096 bytes) and very deeply nested expressions caused
|
|
# Scylla to crash. We now need to check check that expressions in the
|
|
# allowed length (4096 bytes), even if deeply nested, work fine.
|
|
def test_deeply_nested_expression_1(test_table_s):
|
|
# ((((((((((((((((((((((((((( - a syntax error
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
condition = "(" * 4096
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# Continuing the above test, check that Alternator prints a normal "syntax
|
|
# error" for just "(((" but a "expression nested too deeply" for 4096
|
|
# parentheses. This is a Scylla-only test - DynamoDB doesn't make
|
|
# this distinction, and the specific error message is not important.
|
|
# But I wanted to test that Alternator's error messages are as designed.
|
|
def test_deeply_nested_expression_1a(test_table_s, scylla_only):
|
|
p = random_string()
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression.*syntax error'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :x', ExpressionAttributeValues={':x': 1},
|
|
ConditionExpression='(((')
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression.*expression nested too deeply'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :x', ExpressionAttributeValues={':x': 1},
|
|
ConditionExpression='('*4000)
|
|
|
|
# Even if an expression is shorter than 4096 bytes, DynamoDB can reject
|
|
# if it has more than 300 "operators" - in the expression below we fit
|
|
# 909 operators ("<" and "or") under 4096 bytes. DynamoDB rejects this
|
|
# case with the message "Invalid ConditionExpression: The expression
|
|
# contains too many operators; operator count: 301". Scylla currently
|
|
# doesn't have this specific limit, but it rejects this expression because
|
|
# it exceeds nesting depth MAX_DEPTH. The important thing is that the
|
|
# expression is rejected cleanly, without crashing as it used to happen
|
|
# on longer expressions.
|
|
def test_deeply_nested_expression_2(test_table_s):
|
|
# a<b or (a<b or (a<b or (a<b or (...)))):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
# depth=149 has a total of 299 "<" and "or" operators so should work.
|
|
# Importantly, parentheses and spaces are *not* counted among the
|
|
# "operators", only the "<" and "or".
|
|
depth = 149
|
|
condition = "a<b " + "or (a<b "*depth +")"*depth
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
# depth=454 is still below 4096 bytes, but rejected by DynamoDB because
|
|
# it has more than 300 operators, and by Scylla because 454 > MAX_DEPTH.
|
|
depth = 454
|
|
condition = "a<b " + "or (a<b "*depth +")"*depth
|
|
assert len(condition) < 4096
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# Same as above test, but without parentheses, just a lot of OR - like
|
|
# "a<b or a<b or a<b or a<b or ...". Alternator's parser actually implements
|
|
# parsing all these ORs in a loop (a "*" on the "(OR boolean_expression)"),
|
|
# without using recursion, so even if we can fit more than MAX_DEPTH "OR"s in
|
|
# our 4096 byte expression size, such expression is allowed by Scylla.
|
|
# In DynamoDB, there is a limit of 300 on the number of OR operators, so this
|
|
# expression will not be allowed, but we decided not to implement this
|
|
# specific limit in Scylla. So this test is Scylla-only.
|
|
def test_deeply_nested_expression_2a(test_table_s, scylla_only):
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
# repeating the "OR a<b" n=584 times generates an expression which is still
|
|
# just below 4096 bytes. This expression is rejected by DynamoDB because
|
|
# it has more than 300 operators, but because no recursion is involved in
|
|
# parsing it, it is not rejected by Alternator even though n > MAX_DEPTH(=400).
|
|
n = 584
|
|
condition = "a<b " + "or a<b "*n
|
|
assert len(condition) < 4096
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
assert test_table_s.get_item(Key={'p': p}, ConsistentRead=True)['Item']['z'] == 1
|
|
# n=585 results in an expression longer than 4096, so will be rejected
|
|
# because of that limitation.
|
|
n = 585
|
|
condition = "a<b " + "or a<b "*n
|
|
assert len(condition) > 4096
|
|
with pytest.raises(ClientError, match='ValidationException.*expression size'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 1})
|
|
|
|
# Another deeply-recursive expression, (((((((((a<b))))))))), that used to
|
|
# crash Scylla when the expression was very long but shouldn't crash it
|
|
# for expressions shorter than the 4096-byte limit.
|
|
# Currently, DynamoDB and Scylla reject this case with different reasons -
|
|
# DynamoDB complains that "Invalid ConditionExpression: The expression has
|
|
# redundant parentheses", and Scylla stops parsing after recursing too
|
|
# deeply (MAX_DEPTH) and reports "Failed parsing ConditionExpression".
|
|
# The really important thing is Scylla doesn't crash on this expression (as
|
|
# it used to before implementing MAX_DEPTH).
|
|
def test_deeply_nested_expression_3(test_table_s):
|
|
# (((((((((((((((a<b)))))))))))))))
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 2046
|
|
condition = "("*depth + "a<b" + ")"*depth
|
|
assert len(condition) < 4096
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 2})
|
|
|
|
# Another example of a deeply-nested expression which is shorter than
|
|
# the 4096-byte limit, but has more than 300 operators (it has 1000 "NOT"
|
|
# operators), so DynamoDB rejects it and Scylla rejects it because the
|
|
# recursion is deeper than MAX_DEPTH. Of course the more interesting
|
|
# observation is that it doesn't crash Scylla during parsing.
|
|
def test_deeply_nested_expression_4(test_table_s):
|
|
# not not not not ... not not a<b
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 1000 # even, so condition is equivalent to just a<b
|
|
condition = "not "*depth + "a<b"
|
|
assert len(condition) < 4096
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 2})
|
|
|
|
# Another example of a deeply-nested expression shorter than the limit of
|
|
# 4096 bytes, a < f(f(f(f(...(b))))). DynamoDB apparently doesn't count
|
|
# function calls as "operations", so it isn't limited to 300 nested calls.
|
|
# But it should print the correct error message, and obviously not crash.
|
|
def test_deeply_nested_expression_5(test_table_s):
|
|
# a < f(f(f(f(...(b)))))
|
|
p = random_string()
|
|
test_table_s.update_item(Key={'p': p},
|
|
AttributeUpdates={'a': {'Value': 1, 'Action': 'PUT'},
|
|
'b': {'Value': 2, 'Action': 'PUT'}})
|
|
depth = 1355
|
|
condition = "a < " + "f("*depth + "b" + ")"*depth
|
|
assert len(condition) < 4096
|
|
# DynamoDB prints: "Invalid ConditionExpression: Invalid function name;
|
|
# function: f". Scylla fails parsing this expression because the depth
|
|
# exceeds MAX_DEPTH. We think this is fine.
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 2})
|
|
|
|
# a < size(size(size(size(...size(b)))))
|
|
# Here the function exists, but the innermost size returns an integer,
|
|
# which the surrounding size() doesn't like (it's not defined on an int)
|
|
# In Scylla it is still rejected because of MAX_DEPTH.
|
|
depth = 680
|
|
condition = "a < " + "size("*depth + "b" + ")"*depth
|
|
assert len(condition) < 4096
|
|
with pytest.raises(ClientError, match='ValidationException.*ConditionExpression'):
|
|
test_table_s.update_item(Key={'p': p},
|
|
UpdateExpression='SET z = :val',
|
|
ConditionExpression=condition,
|
|
ExpressionAttributeValues={':val': 2})
|
|
|
|
# TODO: additional expression limits documented in DynamoDB's documentation
|
|
# that we should test here:
|
|
# * a limit on the length of attribute or value references (#name or :val) -
|
|
# the reference together with the first character (# or :) is limited to
|
|
# 255 bytes.
|
|
# * the sum of length of ExpressionAttributeValues and ExpressionAttributeNames
|
|
# is limited to 2MB (not a very interesting limit...)
|
|
|
|
#############################################################################
|
|
|
|
# DynamoDB documentation says that the sort key must be between 1 and 1024
|
|
# bytes in length. We already test (test_item.py::test_update_item_empty_key)
|
|
# that 0 bytes are not allowed, so here we want to verify that 1024 is
|
|
# indeed the limit - i.e., 1024 is allowed, 1025 is isn't. This is true for
|
|
# both strings and bytes (and for bytes, it is the actual bytes - not their
|
|
# base64 encoding - that is counted).
|
|
# We may decide that this test never needs to pass on Alternator, because
|
|
# we may adopt a different limit. In this case we'll need to document this
|
|
# decision. In any case, Alternator must have some key-length limits (the
|
|
# internal implementation limits key length to 64 KB), so the test after this
|
|
# one should pass.
|
|
@pytest.mark.xfail(reason="issue #10347: sort key limits not enforced")
|
|
def test_limit_sort_key_len_1024(test_table_ss, test_table_sb):
|
|
p = random_string()
|
|
# String sort key with length 1024 is fine:
|
|
key = {'p': p, 'c': 'x'*1024}
|
|
test_table_ss.put_item(Item=key)
|
|
assert test_table_ss.get_item(Key=key, ConsistentRead=True)['Item'] == key
|
|
# But sort key with length 1025 is forbidden - in both read and write.
|
|
# DynamoDB's message says "Aggregated size of all range keys has exceeded
|
|
# the size limit of 1024 bytes". It's not clear what "all range keys"
|
|
# actually refers to, as there can be only one. We investigate this
|
|
# further below in test_limit_sort_key_len_lsi().
|
|
key = {'p': p, 'c': 'x'*1025}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_ss.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_ss.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# The same limits are true for the bytes type. The length of a bytes
|
|
# array is its real length - not the length of its base64 encoding.
|
|
key = {'p': p, 'c': bytearray([123]*1024)}
|
|
test_table_sb.put_item(Item=key)
|
|
assert test_table_sb.get_item(Key=key, ConsistentRead=True)['Item'] == key
|
|
key = {'p': p, 'c': bytearray([123]*1025)}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_sb.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_sb.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# This is a variant of the above test, where we don't insist that the
|
|
# sort key length limit must be exactly 1024 bytes as in DynamoDB, but
|
|
# that it be *at least* 1024. I.e., we verify that 1024-byte sort keys
|
|
# are allowed, while very long keys that surpass Scylla's low-level
|
|
# key-length limit (64 KB) are forbidden with an appropriate error message
|
|
# and not an "internal server error". This test should pass even if
|
|
# Alternator decides to adopt a different sort-key-length limit from
|
|
# DynamoDB. We do have to adopt *some* limit because the internal Scylla
|
|
# implementation has a 64 KB limit on key lengths.
|
|
@pytest.mark.xfail(reason="issue #10347: sort key limits not enforced")
|
|
def test_limit_sort_key_len(test_table_ss, test_table_sb):
|
|
p = random_string()
|
|
# String sort key with length 1024 is fine:
|
|
key = {'p': p, 'c': 'x'*1024}
|
|
test_table_ss.put_item(Item=key)
|
|
assert test_table_ss.get_item(Key=key, ConsistentRead=True)['Item'] == key
|
|
# Sort key of length 64 KB + 1 is forbidden - it obviously exceeds
|
|
# DynamoDB's limit (1024 bytes), but also exceeds Scylla's internal
|
|
# limit on key length (64 KB). We except to get a reasonable error
|
|
# on request validation - not some "internal server error".
|
|
key = {'p': p, 'c': 'x'*65537}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_ss.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_ss.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# The same limits are true for the bytes type. The length of a bytes
|
|
# array is its real length - not the length of its base64 encoding.
|
|
key = {'p': p, 'c': bytearray([123]*1024)}
|
|
test_table_sb.put_item(Item=key)
|
|
assert test_table_sb.get_item(Key=key, ConsistentRead=True)['Item'] == key
|
|
key = {'p': p, 'c': bytearray([123]*65537)}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_sb.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_sb.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# As mentioned above, DynamoDB's error about sort key length exceeding the
|
|
# 1024 byte limit says that "Aggregated size of all range keys has exceeded
|
|
# the size limit of 1024 bytes". This is an odd message, considering that
|
|
# there can only be one range key... So there is a question whether when we
|
|
# have an LSI and several of the item's attributes become range keys (of
|
|
# different tables), perhaps their *total* length is limited. It turns out
|
|
# the answer is no. We can write an item with two 1024-byte attributes, where
|
|
# one if the base table's sort key and the other is an LSI's sort key.
|
|
# DyanamoDB's error message appears to be nothing more than a mistake.
|
|
def test_limit_sort_key_len_lsi(dynamodb):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'b', 'KeyType': 'RANGE' }
|
|
],
|
|
AttributeDefinitions=[
|
|
{ 'AttributeName': 'a', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'b', 'AttributeType': 'S' },
|
|
{ 'AttributeName': 'c', 'AttributeType': 'S' }
|
|
],
|
|
LocalSecondaryIndexes=[
|
|
{ 'IndexName': 'lsi', 'KeySchema': [
|
|
{ 'AttributeName': 'a', 'KeyType': 'HASH' },
|
|
{ 'AttributeName': 'c', 'KeyType': 'RANGE' },
|
|
], 'Projection': { 'ProjectionType': 'ALL' }
|
|
}
|
|
]) as table:
|
|
item = {'a': 'hello', 'b': 'x'*1024, 'c': 'y'*1024 }
|
|
table.put_item(Item=item)
|
|
assert table.get_item(Key={'a': 'hello', 'b': 'x'*1024}, ConsistentRead=True)['Item'] == item
|
|
assert table.query(IndexName='lsi', KeyConditions={'a': {'AttributeValueList': ['hello'], 'ComparisonOperator': 'EQ'}, 'c': {'AttributeValueList': ['y'*1024], 'ComparisonOperator': 'EQ'}}, ConsistentRead=True)['Items'] == [item]
|
|
|
|
# DynamoDB documentation says that the partition key must be between 1 and 2048
|
|
# bytes in length. We already test (test_item.py::test_update_item_empty_key)
|
|
# that 0 bytes are not allowed, so here we want to verify that 2048 is
|
|
# indeed the limit - i.e., 2048 is allowed, 2049 is isn't. This is true for
|
|
# both strings and bytes (and for bytes, it is the actual bytes - not their
|
|
# base64 encoding - that is counted).
|
|
# We may decide that this test never needs to pass on Alternator, because
|
|
# we may adopt a different limit. In this case we'll need to document this
|
|
# decision. In any case, Alternator must have some key-length limits (the
|
|
# internal implementation limits key length to 64 KB), so even if this test
|
|
# won't pass, the one after it should pass.
|
|
@pytest.mark.xfail(reason="issue #10347: sort key limits not enforced")
|
|
def test_limit_partition_key_len_2048(test_table_s, test_table_b):
|
|
# String partition key with length 2048 is fine:
|
|
item = {'p': 'x'*2048, 'z': 'hello'}
|
|
test_table_s.put_item(Item=item)
|
|
assert test_table_s.get_item(Key={'p': 'x'*2048}, ConsistentRead=True)['Item'] == item
|
|
# But partition key with length 2049 is forbidden - in both read and write.
|
|
key = {'p': 'x'*2049}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_s.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_s.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# The same limits are true for the bytes type. The length of a bytes
|
|
# array is its real length - not the length of its base64 encoding.
|
|
item = {'p': bytearray([123]*2048), 'z': 'hello'}
|
|
test_table_b.put_item(Item=item)
|
|
assert test_table_b.get_item(Key={'p': bytearray([123]*2048)}, ConsistentRead=True)['Item'] == item
|
|
key = {'p': bytearray([123]*2049)}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_b.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_b.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# This is a variant of the above test, where we don't insist that the
|
|
# partition key length limit must be exactly 2048 bytes as in DynamoDB, but
|
|
# that it be *at least* 2048. I.e., we verify that 2048-byte sort keys
|
|
# are allowed, while very long keys that surpass Scylla's low-level
|
|
# key-length limit (64 KB) are forbidden with an appropriate error message
|
|
# and not an "internal server error". This test should pass even if
|
|
# Alternator decides to adopt a different sort-key-length limit from
|
|
# DynamoDB. We do have to adopt *some* limit because the internal Scylla
|
|
# implementation has a 64 KB limit on key lengths.
|
|
@pytest.mark.xfail(reason="issue #10347: sort key limits not enforced")
|
|
def test_limit_partition_key_len(test_table_s, test_table_b):
|
|
# String partition key with length 2048 is fine:
|
|
item = {'p': 'x'*2048, 'z': 'hello'}
|
|
test_table_s.put_item(Item=item)
|
|
assert test_table_s.get_item(Key={'p': 'x'*2048}, ConsistentRead=True)['Item'] == item
|
|
# Partition key of length 64 KB + 1 is forbidden - it obviously exceeds
|
|
# DynamoDB's limit (2048 bytes), but also exceeds Scylla's internal
|
|
# limit on key length (64 KB). We except to get a reasonable error
|
|
# on request validation - not some "internal server error".
|
|
key = {'p': 'x'*65537}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_s.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_s.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# The same limits are true for the bytes type. The length of a bytes
|
|
# array is its real length - not the length of its base64 encoding.
|
|
item = {'p': bytearray([123]*2048), 'z': 'hello'}
|
|
test_table_b.put_item(Item=item)
|
|
assert test_table_b.get_item(Key={'p': bytearray([123]*2048)}, ConsistentRead=True)['Item'] == item
|
|
key = {'p': bytearray([123]*65537)}
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_b.put_item(Item=key)
|
|
with pytest.raises(ClientError, match='ValidationException.*limit'):
|
|
test_table_b.get_item(Key=key, ConsistentRead=True)
|
|
|
|
# Test attr name limit of UpdateTable's GlobalSecondaryIndexUpdates.
|
|
# Reproduces issue #9169.
|
|
def test_limit_gsiu_key_len_bad(dynamodb):
|
|
too_long_name = random_string(256)
|
|
|
|
with pytest.raises(ClientError, match='ValidationException..*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ]) as table:
|
|
# Now use UpdateTable to create the GSI
|
|
dynamodb.meta.client.update_table(TableName=table.name,
|
|
AttributeDefinitions=[{ 'AttributeName': too_long_name, 'AttributeType': 'S' }],
|
|
GlobalSecondaryIndexUpdates=[ { 'Create':
|
|
{ 'IndexName': 'hello',
|
|
'KeySchema': [{ 'AttributeName': too_long_name, 'KeyType': 'HASH' }],
|
|
'Projection': { 'ProjectionType': 'ALL' }
|
|
}}])
|
|
|
|
# Test attr name limit of UpdateTable with GlobalSecondaryIndexUpdates.
|
|
# The tests here cover the cases of too long names in incorrect (incoherent)
|
|
# user requests. The incoherence here means that AttributeDefinitions should
|
|
# refer to AttributeName value that is used inside GlobalSecondaryIndexUpdates
|
|
# as well. In all the cases, DynamoDB returns attrribute name length-related
|
|
# errors, ignoring the issues with the consistency.
|
|
# Reproduces issue #9169.
|
|
def test_limit_gsiu_key_len_bad_incoherent_names(dynamodb):
|
|
too_long_name = random_string(256)
|
|
|
|
with pytest.raises(ClientError, match='ValidationException..*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ]) as table:
|
|
# Now use UpdateTable to create the GSI
|
|
dynamodb.meta.client.update_table(TableName=table.name,
|
|
AttributeDefinitions=[{ 'AttributeName': too_long_name, 'AttributeType': 'S' }],
|
|
GlobalSecondaryIndexUpdates=[ { 'Create':
|
|
{ 'IndexName': 'hello',
|
|
'KeySchema': [{ 'AttributeName': "incoherent-short-name", 'KeyType': 'HASH' }],
|
|
'Projection': { 'ProjectionType': 'ALL' }
|
|
}}])
|
|
|
|
with pytest.raises(ClientError, match='ValidationException..*25[56]'):
|
|
with new_test_table(dynamodb,
|
|
KeySchema=[ { 'AttributeName': 'p', 'KeyType': 'HASH' } ],
|
|
AttributeDefinitions=[ { 'AttributeName': 'p', 'AttributeType': 'S' } ]) as table:
|
|
# Now use UpdateTable to create the GSI
|
|
dynamodb.meta.client.update_table(TableName=table.name,
|
|
AttributeDefinitions=[{ 'AttributeName': "incoherent-short-name", 'AttributeType': 'S' }],
|
|
GlobalSecondaryIndexUpdates=[ { 'Create':
|
|
{ 'IndexName': 'hello',
|
|
'KeySchema': [{ 'AttributeName': too_long_name, 'KeyType': 'HASH' }],
|
|
'Projection': { 'ProjectionType': 'ALL' }
|
|
}}])
|