From b4e3d4ac2f1bc5587f44731a218e2f73452be0d3 Mon Sep 17 00:00:00 2001 From: Nadav Har'El Date: Wed, 10 Sep 2025 15:37:40 +0300 Subject: [PATCH] alternator: nicer error message for integer overflow in list index In the DynamoDB API, when "a" is a list attribute, a[999] returns the 1000th element. But if the list isn't that long (e.g., it only has 5 elements), a[999] returns nothing - it's not an error. But it turns out that when the index is so long that it can't even be parsed as an integer, e.g., 99999999999999, DynamoDB does report an error: Invalid ProjectionExpression: List index is not within the allowable range; index: [99999999999999] Before this patch, Alternator also returned an error in this case, with the right type (ValidationException), but with a strange low-level error text: Failed parsing ProjectionExpression 'a[99999999999999]': std::out_of_range (stoi) The problem was that the code (in alternator/expressions.g) ran stoi() without converting its std::out_of_range exception to a better user-facing message. We do this in this patch, and the error message now looks like: Failed parsing ProjectionExpression 'a[99999999999999]': list index out of integer range This patch also includes a test reproducing this error, which passes on DynamDB and on Alternator it fails before this patch and passes with the patch. Fixes #25947 Signed-off-by: Nadav Har'El Closes scylladb/scylladb#25951 --- alternator/expressions.g | 8 ++++- test/alternator/test_projection_expression.py | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/alternator/expressions.g b/alternator/expressions.g index c3ac1f4837..cf20bb640b 100644 --- a/alternator/expressions.g +++ b/alternator/expressions.g @@ -184,7 +184,13 @@ path_component: NAME | NAMEREF; path returns [parsed::path p]: root=path_component { $p.set_root($root.text); } ( '.' name=path_component { $p.add_dot($name.text); } - | '[' INTEGER ']' { $p.add_index(std::stoi($INTEGER.text)); } + | '[' INTEGER ']' { + try { + $p.add_index(std::stoi($INTEGER.text)); + } catch(std::out_of_range&) { + throw expressions_syntax_error("list index out of integer range"); + } + } )*; /* See comment above why the "depth" counter was needed here */ diff --git a/test/alternator/test_projection_expression.py b/test/alternator/test_projection_expression.py index 395d2f69bf..9343fe5f6f 100644 --- a/test/alternator/test_projection_expression.py +++ b/test/alternator/test_projection_expression.py @@ -347,3 +347,34 @@ def test_projection_expression_path_nesting_levels(test_table_s): # nesting levels: 33". with pytest.raises(ClientError, match='ValidationException.*nesting levels'): test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a'+('.b'*32)) + +# Above we already checked different cases of reading individual elements +# from a list - the expression a[i]. The following test exercises these +# list indexes more rigourously, including testing what happens when the +# index overflows an integer (reproducing #25947). +def test_projection_expression_list_index(test_table_s): + p = random_string() + test_table_s.put_item(Item={'p': p, 'a': [7, 42]}) + # a[0] and a[1] return the elements from the list, as expected + # (note that a[i] actually returns an array with a single element a[i]) + assert {'a': [7]} == test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[0]')['Item'] + assert {'a': [42]} == test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[1]')['Item'] + # If the index is beyond the length of the array, such as a[2] or a[999], + # we expect to get back an empty Item - not an error, and not missing Item. + assert {} == test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[2]')['Item'] + assert {} == test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[999]')['Item'] + # If the index is so high that it can't be parsed as an integer, it isn't + # silently ignored like 999 above, but causes a parse error. DynamoDB + # reports: "Invalid ProjectionExpression: List index is not within the + # allowable range; index: [99999999999999]". After fixing #25947, + # Alternator reports: "Failed parsing ProjectionExpression + # 'a[99999999999999]': list index out of integer range". + with pytest.raises(ClientError, match='ValidationException.*ProjectionExpression.*index'): + test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[99999999999999]')['Item'] + # Trying to use a negative number as an index, like a[-1], is just a + # syntax error - the parser expects to see digits, not "-". + with pytest.raises(ClientError, match='ValidationException.*ProjectionExpression.*[Ss]yntax error'): + test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[-1]')['Item'] + # A completely missing index - a[] - is also a syntax error: + with pytest.raises(ClientError, match='ValidationException.*ProjectionExpression.*[Ss]yntax error'): + test_table_s.get_item(Key={'p': p}, ConsistentRead=True, ProjectionExpression='a[]')['Item']