diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py index 019e8eece..9be813030 100644 --- a/moto/dynamodb/models/__init__.py +++ b/moto/dynamodb/models/__init__.py @@ -316,7 +316,7 @@ class DynamoDBBackend(BaseBackend): limit: int, exclusive_start_key: Dict[str, Any], scan_index_forward: bool, - projection_expression: str, + projection_expression: Optional[str], index_name: Optional[str] = None, expr_names: Optional[Dict[str, str]] = None, expr_values: Optional[Dict[str, str]] = None, @@ -351,11 +351,11 @@ class DynamoDBBackend(BaseBackend): filters: Dict[str, Any], limit: int, exclusive_start_key: Dict[str, Any], - filter_expression: str, + filter_expression: Optional[str], expr_names: Dict[str, Any], expr_values: Dict[str, Any], index_name: str, - projection_expression: str, + projection_expression: Optional[str], ) -> Tuple[List[Item], int, Optional[Dict[str, Any]]]: table = self.get_table(table_name) diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py index 4d433ea7c..78f18e809 100644 --- a/moto/dynamodb/models/table.py +++ b/moto/dynamodb/models/table.py @@ -637,7 +637,7 @@ class Table(CloudFormationModel): limit: int, exclusive_start_key: Dict[str, Any], scan_index_forward: bool, - projection_expression: str, + projection_expression: Optional[str], index_name: Optional[str] = None, filter_expression: Any = None, **filter_kwargs: Any, diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 1adc7430b..8f94fcb6f 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -351,6 +351,22 @@ class DynamoHandler(BaseResponse): + dump_list(actual_attrs) ) + def _get_filter_expression(self) -> Optional[str]: + filter_expression = self.body.get("FilterExpression") + if filter_expression == "": + raise MockValidationException( + "Invalid FilterExpression: The expression can not be empty;" + ) + return filter_expression + + def _get_projection_expression(self) -> Optional[str]: + expression = self.body.get("ProjectionExpression") + if expression == "": + raise MockValidationException( + "Invalid ProjectionExpression: The expression can not be empty;" + ) + return expression + def delete_table(self) -> str: name = self.body["TableName"] table = self.dynamodb_backend.delete_table(name) @@ -521,7 +537,7 @@ class DynamoHandler(BaseResponse): f"empty string value. Key: {empty_keys[0]}" ) - projection_expression = self.body.get("ProjectionExpression") + projection_expression = self._get_projection_expression() attributes_to_get = self.body.get("AttributesToGet") if projection_expression and attributes_to_get: raise MockValidationException( @@ -631,9 +647,9 @@ class DynamoHandler(BaseResponse): def query(self) -> str: name = self.body["TableName"] key_condition_expression = self.body.get("KeyConditionExpression") - projection_expression = self.body.get("ProjectionExpression") + projection_expression = self._get_projection_expression() expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) - filter_expression = self.body.get("FilterExpression") + filter_expression = self._get_filter_expression() expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) projection_expression = self._adjust_projection_expression( @@ -726,8 +742,8 @@ class DynamoHandler(BaseResponse): return dynamo_json_dump(result) def _adjust_projection_expression( - self, projection_expression: str, expr_attr_names: Dict[str, str] - ) -> str: + self, projection_expression: Optional[str], expr_attr_names: Dict[str, str] + ) -> Optional[str]: def _adjust(expression: str) -> str: return ( expr_attr_names[expression] @@ -762,10 +778,10 @@ class DynamoHandler(BaseResponse): comparison_values = scan_filter.get("AttributeValueList", []) filters[attribute_name] = (comparison_operator, comparison_values) - filter_expression = self.body.get("FilterExpression") + filter_expression = self._get_filter_expression() expression_attribute_values = self.body.get("ExpressionAttributeValues", {}) expression_attribute_names = self.body.get("ExpressionAttributeNames", {}) - projection_expression = self.body.get("ProjectionExpression", "") + projection_expression = self._get_projection_expression() exclusive_start_key = self.body.get("ExclusiveStartKey") limit = self.body.get("Limit") index_name = self.body.get("IndexName") diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py index ad08f8322..97e7d9e2f 100644 --- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py +++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py @@ -144,7 +144,7 @@ def test_empty_expressionattributenames_with_empty_projection(): table = ddb.Table("test-table") with pytest.raises(ClientError) as exc: table.get_item( - Key={"id": "my_id"}, ProjectionExpression="", ExpressionAttributeNames={} + Key={"id": "my_id"}, ProjectionExpression="a", ExpressionAttributeNames={} ) err = exc.value.response["Error"] assert err["Code"] == "ValidationException" @@ -1067,3 +1067,30 @@ def test_list_append_errors_for_unknown_attribute_value(): ExpressionAttributeValues={":i": {"L": [{"S": "bar2"}]}}, ReturnValues="UPDATED_NEW", ) + + +@mock_dynamodb +def test_query_with_empty_filter_expression(): + ddb = boto3.resource("dynamodb", region_name="us-east-1") + ddb.create_table( + TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema + ) + table = ddb.Table("test-table") + with pytest.raises(ClientError) as exc: + table.query( + KeyConditionExpression="partitionKey = sth", ProjectionExpression="" + ) + err = exc.value.response["Error"] + assert err["Code"] == "ValidationException" + assert ( + err["Message"] + == "Invalid ProjectionExpression: The expression can not be empty;" + ) + + with pytest.raises(ClientError) as exc: + table.query(KeyConditionExpression="partitionKey = sth", FilterExpression="") + err = exc.value.response["Error"] + assert err["Code"] == "ValidationException" + assert ( + err["Message"] == "Invalid FilterExpression: The expression can not be empty;" + )