Describe the bug
The binary condition operators in boto3.dynamodb.conditions, such as And and Or, don't correctly handle being given three or more arguments. This is not caught client-side, and causes a pretty cryptic error.
Expected Behavior
Either the operators are handled correctly (ideally with type hinting), or a more relevant error is raised
Current Behavior
Errors are like
botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the Scan operation: Value provided in ExpressionAttributeNames unused in expressions: keys: {#n2}
the generated request body from the below SSCCE is
{"TableName": "my_table", "FilterExpression": "(#n0 > :v0 AND #n1 < :v1)", "ExpressionAttributeNames": {"#n0": "age", "#n1": "age", "#n2": "height"}, "ExpressionAttributeValues": {":v0": {"N": "20"}, ":v1": {"N": "30"}, ":v2": {"N": "180"}}}
which is indeed invalid as indicated in the error message.
Reproduction Steps
Example I would expect to work but doesn't:
import boto3
from boto3.dynamodb.conditions import Attr, And
# Table doesn't need to exist to reproduce, since request is validated syntactically before hitting table
table_name = "my_table"
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(table_name)
response = table.scan(
FilterExpression=And(
Attr("age").gt(20),
Attr("age").lt(30),
Attr("height").gt(180),
)
)
Current workaround:
import boto3
from boto3.dynamodb.conditions import Attr, And
from functools import reduce
# Table does need to exist if you want a successful response!
table_name = "my_table"
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(table_name)
response = table.scan(
FilterExpression=reduce(
And, [
Attr("age").gt(20),
Attr("age").lt(30),
Attr("height").gt(180),
]
)
)
Possible Solution
Explanation:
The conditions all inherit from ConditionBase, which has properties expression_operator (the character in the DynamoDB condition expression syntax used to indicate the relevant operator), values (the operands), and expression_format (a string with python format fields {operator} and some of {0}, {1}, {2}).
The expression_operator and expression_format are defined by the operator, and the values are set on construction. For example, And:
|
class And(ConditionBase): |
|
expression_operator = 'AND' |
|
expression_format = '({0} {operator} {1})' |
Note that ConditionBase's constructor takes an arbitrary number of values.
The number of values passed to the condition operator is never validated, which is the root cause of this error. The core of the expression builder is
|
def _build_expression( |
|
self, |
|
condition, |
|
attribute_name_placeholders, |
|
attribute_value_placeholders, |
|
is_key_condition, |
|
): |
|
expression_dict = condition.get_expression() |
|
replaced_values = [] |
|
for value in expression_dict['values']: |
|
# Build the necessary placeholders for that value. |
|
# Placeholders are built for both attribute names and values. |
|
replaced_value = self._build_expression_component( |
|
value, |
|
attribute_name_placeholders, |
|
attribute_value_placeholders, |
|
condition.has_grouped_values, |
|
is_key_condition, |
|
) |
|
replaced_values.append(replaced_value) |
|
# Fill out the expression using the operator and the |
|
# values that have been replaced with placeholders. |
|
return expression_dict['format'].format( |
|
*replaced_values, operator=expression_dict['operator'] |
|
) |
Note that every value passed (the number of which is never checked) gets a placeholder value, but only as many as there are number format fields in expression_format will actually be included in the final condition expression (since Python's str.format() silently ignores extraneous arguments).
Possible fix:
Add a property arity or number_of_values to ConditionBase, and have the constructor validate that a valid number has been received. Optionally, for quality of life also expand the And and Or constructs to take an arbitrary number of arguments (either by transparently expanding to "{0} AND {1} AND {2} AND {3}" or "((({0} AND {1}) AND {2}) AND {3})")
Additional Information/Context
No response
SDK version used
1.34.97
Environment details (OS name and version, etc.)
Windows 10
Describe the bug
The binary condition operators in
boto3.dynamodb.conditions, such asAndandOr, don't correctly handle being given three or more arguments. This is not caught client-side, and causes a pretty cryptic error.Expected Behavior
Either the operators are handled correctly (ideally with type hinting), or a more relevant error is raised
Current Behavior
Errors are like
the generated request body from the below SSCCE is
{"TableName": "my_table", "FilterExpression": "(#n0 > :v0 AND #n1 < :v1)", "ExpressionAttributeNames": {"#n0": "age", "#n1": "age", "#n2": "height"}, "ExpressionAttributeValues": {":v0": {"N": "20"}, ":v1": {"N": "30"}, ":v2": {"N": "180"}}}which is indeed invalid as indicated in the error message.
Reproduction Steps
Example I would expect to work but doesn't:
Current workaround:
Possible Solution
Explanation:
The conditions all inherit from ConditionBase, which has properties
expression_operator(the character in the DynamoDB condition expression syntax used to indicate the relevant operator),values(the operands), andexpression_format(a string with python format fields{operator}and some of{0},{1},{2}).The
expression_operatorandexpression_formatare defined by the operator, and the values are set on construction. For example,And:boto3/boto3/dynamodb/conditions.py
Lines 226 to 228 in 096e458
Note that
ConditionBase's constructor takes an arbitrary number of values.The number of values passed to the condition operator is never validated, which is the root cause of this error. The core of the expression builder is
boto3/boto3/dynamodb/conditions.py
Lines 359 to 383 in 096e458
Note that every value passed (the number of which is never checked) gets a placeholder value, but only as many as there are number format fields in
expression_formatwill actually be included in the final condition expression (since Python'sstr.format()silently ignores extraneous arguments).Possible fix:
Add a property
arityornumber_of_valuestoConditionBase, and have the constructor validate that a valid number has been received. Optionally, for quality of life also expand theAndandOrconstructs to take an arbitrary number of arguments (either by transparently expanding to"{0} AND {1} AND {2} AND {3}"or"((({0} AND {1}) AND {2}) AND {3})")Additional Information/Context
No response
SDK version used
1.34.97
Environment details (OS name and version, etc.)
Windows 10