Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 23 additions & 3 deletions moto/dynamodb/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,8 +785,6 @@ def execute_statement(
self, statement: str, parameters: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
Only SELECT-statements are supported for now.

Pagination is not yet implemented.

Parsing is highly experimental - please raise an issue if you find any bugs.
Expand All @@ -799,7 +797,29 @@ def execute_statement(
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(
self, statements: List[Dict[str, Any]]
Expand Down
7 changes: 5 additions & 2 deletions moto/dynamodb/parsing/partiql.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from typing import TYPE_CHECKING, Any, Dict, List
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple

if TYPE_CHECKING:
from py_partiql_parser import QueryMetadata


def query(
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

return DynamoDBStatementParser(source_data).parse(statement, parameters)
Expand Down
18 changes: 9 additions & 9 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ all =
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93
setuptools
multipart
Expand All @@ -71,7 +71,7 @@ proxy =
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93
setuptools
multipart
Expand All @@ -86,7 +86,7 @@ server =
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93
setuptools
flask!=2.2.0,!=2.2.1
Expand Down Expand Up @@ -121,7 +121,7 @@ cloudformation =
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
aws-xray-sdk!=0.96,>=0.93
setuptools
cloudfront =
Expand All @@ -144,10 +144,10 @@ dms =
ds = sshpubkeys>=3.1.0
dynamodb =
docker>=3.0.0
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
dynamodbstreams =
docker>=3.0.0
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
ebs = sshpubkeys>=3.1.0
ec2 = sshpubkeys>=3.1.0
ec2instanceconnect =
Expand Down Expand Up @@ -210,15 +210,15 @@ resourcegroupstaggingapi =
openapi-spec-validator>=0.5.0
pyparsing>=3.0.7
jsondiff>=1.1.2
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
route53 =
route53resolver = sshpubkeys>=3.1.0
s3 =
PyYAML>=5.1
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
s3crc32c =
PyYAML>=5.1
py-partiql-parser==0.4.2
py-partiql-parser==0.5.0
crc32c
s3control =
sagemaker =
Expand Down
106 changes: 106 additions & 0 deletions tests/test_dynamodb/test_dynamodb_statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
items = dynamodb_client.execute_statement(Statement=partiql_statement)["Items"]
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"] == []