DynamoDB: execute_statement() now supports INSERT/UPDATE/DELETE queries (#7130)

This commit is contained in:
Bert Blommers 2023-12-17 09:53:16 -01:00 committed by GitHub
parent 2228f07b80
commit 9c39ab9c77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 14 deletions

View File

@ -785,8 +785,6 @@ class DynamoDBBackend(BaseBackend):
self, statement: str, parameters: List[Dict[str, Any]] self, statement: str, parameters: List[Dict[str, Any]]
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
Only SELECT-statements are supported for now.
Pagination is not yet implemented. Pagination is not yet implemented.
Parsing is highly experimental - please raise an issue if you find any bugs. Parsing is highly experimental - please raise an issue if you find any bugs.
@ -799,7 +797,29 @@ class DynamoDBBackend(BaseBackend):
item.to_json()["Attributes"] for item in table.all_items() item.to_json()["Attributes"] for item in table.all_items()
] ]
return partiql.query(statement, source_data, parameters) return_data, updates_per_table = partiql.query(
statement, source_data, parameters
)
for table_name, updates in updates_per_table.items():
table = self.tables[table_name]
for before, after in updates:
if after is None and before is not None:
# DELETE
hash_key = DynamoType(before[table.hash_key_attr])
if table.range_key_attr:
range_key = DynamoType(before[table.range_key_attr])
else:
range_key = None
table.delete_item(hash_key, range_key)
elif before is None and after is not None:
# CREATE
table.put_item(after)
elif before is not None and after is not None:
# UPDATE
table.put_item(after)
return return_data
def execute_transaction( def execute_transaction(
self, statements: List[Dict[str, Any]] self, statements: List[Dict[str, Any]]

View File

@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Dict, List from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
if TYPE_CHECKING: if TYPE_CHECKING:
from py_partiql_parser import QueryMetadata from py_partiql_parser import QueryMetadata
@ -6,7 +6,10 @@ if TYPE_CHECKING:
def query( def query(
statement: str, source_data: Dict[str, str], parameters: List[Dict[str, Any]] statement: str, source_data: Dict[str, str], parameters: List[Dict[str, Any]]
) -> List[Dict[str, Any]]: ) -> Tuple[
List[Dict[str, Any]],
Dict[str, List[Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]]],
]:
from py_partiql_parser import DynamoDBStatementParser from py_partiql_parser import DynamoDBStatementParser
return DynamoDBStatementParser(source_data).parse(statement, parameters) return DynamoDBStatementParser(source_data).parse(statement, parameters)

View File

@ -56,7 +56,7 @@ all =
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93 aws-xray-sdk!=0.96,>=0.93
setuptools setuptools
multipart multipart
@ -71,7 +71,7 @@ proxy =
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93 aws-xray-sdk!=0.96,>=0.93
setuptools setuptools
multipart multipart
@ -86,7 +86,7 @@ server =
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93 aws-xray-sdk!=0.96,>=0.93
setuptools setuptools
flask!=2.2.0,!=2.2.1 flask!=2.2.0,!=2.2.1
@ -121,7 +121,7 @@ cloudformation =
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93 aws-xray-sdk!=0.96,>=0.93
setuptools setuptools
cloudfront = cloudfront =
@ -144,10 +144,10 @@ dms =
ds = sshpubkeys>=3.1.0 ds = sshpubkeys>=3.1.0
dynamodb = dynamodb =
docker>=3.0.0 docker>=3.0.0
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
dynamodbstreams = dynamodbstreams =
docker>=3.0.0 docker>=3.0.0
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
ebs = sshpubkeys>=3.1.0 ebs = sshpubkeys>=3.1.0
ec2 = sshpubkeys>=3.1.0 ec2 = sshpubkeys>=3.1.0
ec2instanceconnect = ec2instanceconnect =
@ -210,15 +210,15 @@ resourcegroupstaggingapi =
openapi-spec-validator>=0.5.0 openapi-spec-validator>=0.5.0
pyparsing>=3.0.7 pyparsing>=3.0.7
jsondiff>=1.1.2 jsondiff>=1.1.2
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
route53 = route53 =
route53resolver = sshpubkeys>=3.1.0 route53resolver = sshpubkeys>=3.1.0
s3 = s3 =
PyYAML>=5.1 PyYAML>=5.1
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
s3crc32c = s3crc32c =
PyYAML>=5.1 PyYAML>=5.1
py-partiql-parser==0.4.2 py-partiql-parser==0.5.0
crc32c crc32c
s3control = s3control =
sagemaker = sagemaker =

View File

@ -260,3 +260,109 @@ def test_execute_statement_with_all_clauses(table_name=None):
partiql_statement = f"SELECT pk FROM \"{table_name}\" WHERE (contains(\"NameLower\", 'code') OR contains(\"DescriptionLower\", 'code')) AND Category = 'free' AND Price >= 0 AND Price <= 1 AND FreeTier IS NOT MISSING AND attribute_type(\"FreeTier\", 'N')" partiql_statement = f"SELECT pk FROM \"{table_name}\" WHERE (contains(\"NameLower\", 'code') OR contains(\"DescriptionLower\", 'code')) AND Category = 'free' AND Price >= 0 AND Price <= 1 AND FreeTier IS NOT MISSING AND attribute_type(\"FreeTier\", 'N')"
items = dynamodb_client.execute_statement(Statement=partiql_statement)["Items"] items = dynamodb_client.execute_statement(Statement=partiql_statement)["Items"]
assert items == [{"pk": {"S": "0"}}] assert items == [{"pk": {"S": "0"}}]
@pytest.mark.aws_verified
@dynamodb_aws_verified()
def test_insert_data(table_name=None):
client = boto3.client("dynamodb", "us-east-1")
create_items(table_name)
resp = client.execute_statement(
Statement=f"INSERT INTO \"{table_name}\" value {{'pk': 'msg3'}}"
)
assert resp["Items"] == []
items = client.scan(TableName=table_name)["Items"]
assert len(items) == 3
assert {"pk": {"S": "msg3"}} in items
# More advanced insertion
client.execute_statement(
Statement=f"INSERT INTO \"{table_name}\" value {{'pk': 'msg4', 'attr':{{'sth': ['other']}}}}"
)
items = client.scan(TableName=table_name)["Items"]
assert len(items) == 4
assert {
"pk": {"S": "msg4"},
"attr": {"M": {"sth": {"L": [{"S": "other"}]}}},
} in items
@pytest.mark.aws_verified
@dynamodb_aws_verified()
def test_update_data(table_name=None):
client = boto3.client("dynamodb", "us-east-1")
create_items(table_name)
items = client.scan(TableName=table_name)["Items"]
assert item1 in items
assert item2 in items # unchanged
# Update existing attr
client.execute_statement(
Statement=f"UPDATE \"{table_name}\" SET body='other' WHERE pk='msg1'"
)
items = client.scan(TableName=table_name)["Items"]
assert len(items) == 2
updated_item = item1.copy()
updated_item["body"] = {"S": "other"}
assert updated_item in items
assert item2 in items # unchanged
# Set new attr
client.execute_statement(
Statement=f"UPDATE \"{table_name}\" SET new_attr='asdf' WHERE pk='msg1'"
)
items = client.scan(TableName=table_name)["Items"]
assert len(items) == 2
updated_item["new_attr"] = {"S": "asdf"}
assert updated_item in items
assert item2 in items
# Remove attr
client.execute_statement(
Statement=f"UPDATE \"{table_name}\" REMOVE new_attr WHERE pk='msg1'"
)
items = client.scan(TableName=table_name)["Items"]
assert len(items) == 2
updated_item.pop("new_attr")
assert updated_item in items
assert item2 in items
@pytest.mark.aws_verified
@dynamodb_aws_verified()
def test_delete_data(table_name=None):
client = boto3.client("dynamodb", "us-east-1")
create_items(table_name)
client.execute_statement(Statement=f"DELETE FROM \"{table_name}\" WHERE pk='msg1'")
items = client.scan(TableName=table_name)["Items"]
assert items == [item2]
@mock_dynamodb
def test_delete_data__with_sort_key():
client = boto3.client("dynamodb", "us-east-1")
client.create_table(
TableName="test",
AttributeDefinitions=[
{"AttributeName": "pk", "AttributeType": "S"},
{"AttributeName": "sk", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "pk", "KeyType": "HASH"},
{"AttributeName": "sk", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
)
client.put_item(TableName="test", Item={"pk": {"S": "msg"}, "sk": {"S": "sth"}})
client.execute_statement(Statement="DELETE FROM \"test\" WHERE pk='msg'")
assert client.scan(TableName="test")["Items"] == []