Feature: RDS Data (#6168)

This commit is contained in:
Bert Blommers 2023-04-03 17:18:10 +01:00 committed by GitHub
parent 02e892e30a
commit 52870d6114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 328 additions and 6 deletions

View File

@ -5233,6 +5233,18 @@
- [ ] switchover_read_replica
</details>
## rds-data
<details>
<summary>16% implemented</summary>
- [ ] batch_execute_statement
- [ ] begin_transaction
- [ ] commit_transaction
- [ ] execute_sql
- [X] execute_statement
- [ ] rollback_transaction
</details>
## redshift
<details>
<summary>25% implemented</summary>
@ -6999,7 +7011,6 @@
- qldb
- qldb-session
- rbin
- rds-data
- redshift-serverless
- resiliencehub
- resource-explorer-2
@ -7056,4 +7067,4 @@
- workspaces
- workspaces-web
- xray
</details>
</details>

View File

@ -85,7 +85,7 @@ athena
}
resp = requests.post(
"http://motoapi.amazonaws.com:5000/moto-api/static/athena/query-results",
json=athena_result,
json=expected_results,
)
resp.status_code.should.equal(201)

View File

@ -0,0 +1,74 @@
.. _implementedservice_rds-data:
.. |start-h3| raw:: html
<h3>
.. |end-h3| raw:: html
</h3>
========
rds-data
========
.. autoclass:: moto.rdsdata.models.RDSDataServiceBackend
|start-h3| Example usage |end-h3|
.. sourcecode:: python
@mock_rdsdata
def test_rdsdata_behaviour:
boto3.client("rds-data")
...
|start-h3| Implemented features for this service |end-h3|
- [ ] batch_execute_statement
- [ ] begin_transaction
- [ ] commit_transaction
- [ ] execute_sql
- [X] execute_statement
There is no validation yet on any of the input parameters.
SQL statements are not executed by Moto, so this call will always return 0 records by default.
You can use a dedicated API to override this, by configuring a queue of expected results.
A request to `execute_statement` will take the first result from that queue, and assign it to the provided SQL-query. Subsequent requests using the same SQL-query will return the same result. Other requests using a different SQL-query will take the next result from the queue, or return an empty result if the queue is empty.
Configure this queue by making an HTTP request to `/moto-api/static/rds-data/statement-results`. An example invocation looks like this:
.. sourcecode:: python
expected_results = {
"account_id": "123456789012", # This is the default - can be omitted
"region": "us-east-1", # This is the default - can be omitted
"results": [
{
"records": [...],
"columnMetadata": [...],
"numberOfRecordsUpdated": 42,
"generatedFields": [...],
"formattedRecords": "some json"
},
# other results as required
],
}
resp = requests.post(
"http://motoapi.amazonaws.com:5000/moto-api/static/rds-data/statement-results",
json=expected_results,
)
resp.status_code.should.equal(201)
rdsdata = boto3.client("rds-data", region_name="us-east-1")
resp = rdsdata.execute_statement(resourceArn="not applicable", secretArn="not applicable", sql="SELECT some FROM thing")
- [ ] rollback_transaction

View File

@ -123,6 +123,7 @@ mock_polly = lazy_load(".polly", "mock_polly")
mock_quicksight = lazy_load(".quicksight", "mock_quicksight")
mock_ram = lazy_load(".ram", "mock_ram")
mock_rds = lazy_load(".rds", "mock_rds")
mock_rdsdata = lazy_load(".rdsdata", "mock_rdsdata")
mock_redshift = lazy_load(".redshift", "mock_redshift")
mock_redshiftdata = lazy_load(
".redshiftdata", "mock_redshiftdata", boto3_name="redshift-data"

View File

@ -259,7 +259,7 @@ class AthenaBackend(BaseBackend):
}
resp = requests.post(
"http://motoapi.amazonaws.com:5000/moto-api/static/athena/query-results",
json=athena_result,
json=expected_results,
)
resp.status_code.should.equal(201)

View File

@ -121,6 +121,7 @@ backend_url_patterns = [
("ram", re.compile("https?://ram\\.(.+)\\.amazonaws.com")),
("rds", re.compile("https?://rds\\.(.+)\\.amazonaws\\.com")),
("rds", re.compile("https?://rds\\.amazonaws\\.com")),
("rdsdata", re.compile("https?://rds-data\\.(.+)\\.amazonaws\\.com")),
("redshift", re.compile("https?://redshift\\.(.+)\\.amazonaws\\.com")),
("redshift-data", re.compile("https?://redshift-data\\.(.+)\\.amazonaws\\.com")),
("rekognition", re.compile("https?://rekognition\\.(.+)\\.amazonaws\\.com")),

View File

@ -1,6 +1,6 @@
from moto.core import BaseBackend, DEFAULT_ACCOUNT_ID
from moto.core.model_instances import reset_model_data
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional
class MotoAPIBackend(BaseBackend):
@ -46,5 +46,28 @@ class MotoAPIBackend(BaseBackend):
results = QueryResults(rows=rows, column_info=column_info)
backend.query_results_queue.append(results)
def set_rds_data_result(
self,
records: Optional[List[List[Dict[str, Any]]]],
column_metadata: Optional[List[Dict[str, Any]]],
nr_of_records_updated: Optional[int],
generated_fields: Optional[List[Dict[str, Any]]],
formatted_records: Optional[str],
account_id: str,
region: str,
) -> None:
from moto.rdsdata.models import rdsdata_backends, QueryResults
backend = rdsdata_backends[account_id][region]
backend.results_queue.append(
QueryResults(
records=records,
column_metadata=column_metadata,
number_of_records_updated=nr_of_records_updated,
generated_fields=generated_fields,
formatted_records=formatted_records,
)
)
moto_api_backend = MotoAPIBackend(region_name="global", account_id=DEFAULT_ACCOUNT_ID)

View File

@ -167,3 +167,34 @@ class MotoAPIResponse(BaseResponse):
region=region,
)
return 201, {}, ""
def set_rds_data_result(
self,
request: Any,
full_url: str, # pylint: disable=unused-argument
headers: Any,
) -> TYPE_RESPONSE:
from .models import moto_api_backend
request_body_size = int(headers["Content-Length"])
body = request.environ["wsgi.input"].read(request_body_size).decode("utf-8")
body = json.loads(body)
account_id = body.get("account_id", DEFAULT_ACCOUNT_ID)
region = body.get("region", "us-east-1")
for result in body.get("results", []):
records = result.get("records")
column_metadata = result.get("columnMetadata")
nr_of_records_updated = result.get("numberOfRecordsUpdated")
generated_fields = result.get("generatedFields")
formatted_records = result.get("formattedRecords")
moto_api_backend.set_rds_data_result(
records=records,
column_metadata=column_metadata,
nr_of_records_updated=nr_of_records_updated,
generated_fields=generated_fields,
formatted_records=formatted_records,
account_id=account_id,
region=region,
)
return 201, {}, ""

View File

@ -13,6 +13,7 @@ url_paths = {
"{0}/moto-api/reset-auth": response_instance.reset_auth_response,
"{0}/moto-api/seed": response_instance.seed,
"{0}/moto-api/static/athena/query-results": response_instance.set_athena_result,
"{0}/moto-api/static/rds-data/statement-results": response_instance.set_rds_data_result,
"{0}/moto-api/state-manager/get-transition": response_instance.get_transition,
"{0}/moto-api/state-manager/set-transition": response_instance.set_transition,
"{0}/moto-api/state-manager/unset-transition": response_instance.unset_transition,

5
moto/rdsdata/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""rdsdata module initialization; sets value for base decorator."""
from .models import rdsdata_backends
from ..core.models import base_decorator
mock_rdsdata = base_decorator(rdsdata_backends)

85
moto/rdsdata/models.py Normal file
View File

@ -0,0 +1,85 @@
from moto.core import BaseBackend, BackendDict
from typing import Any, Dict, List, Optional, Tuple
class QueryResults:
def __init__(
self,
records: Optional[List[List[Dict[str, Any]]]] = None,
column_metadata: Optional[List[Dict[str, Any]]] = None,
number_of_records_updated: Optional[int] = None,
generated_fields: Optional[List[Dict[str, Any]]] = None,
formatted_records: Optional[str] = None,
):
self.records = records
self.column_metadata = column_metadata
self.number_of_records_updated = number_of_records_updated
self.generated_fields = generated_fields
self.formatted_records = formatted_records
def to_json(self) -> Dict[str, Any]:
return {
"records": self.records,
"columnMetadata": self.column_metadata,
"numberOfRecordsUpdated": self.number_of_records_updated,
"generatedFields": self.generated_fields,
"formattedRecords": self.formatted_records,
}
class RDSDataServiceBackend(BaseBackend):
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
self.results_queue: List[QueryResults] = []
self.sql_results: Dict[Tuple[str, str], QueryResults] = dict()
def execute_statement(self, resource_arn: str, sql: str) -> QueryResults:
"""
There is no validation yet on any of the input parameters.
SQL statements are not executed by Moto, so this call will always return 0 records by default.
You can use a dedicated API to override this, by configuring a queue of expected results.
A request to `execute_statement` will take the first result from that queue, and assign it to the provided SQL-query. Subsequent requests using the same SQL-query will return the same result. Other requests using a different SQL-query will take the next result from the queue, or return an empty result if the queue is empty.
Configure this queue by making an HTTP request to `/moto-api/static/rds-data/statement-results`. An example invocation looks like this:
.. sourcecode:: python
expected_results = {
"account_id": "123456789012", # This is the default - can be omitted
"region": "us-east-1", # This is the default - can be omitted
"results": [
{
"records": [...],
"columnMetadata": [...],
"numberOfRecordsUpdated": 42,
"generatedFields": [...],
"formattedRecords": "some json"
},
# other results as required
],
}
resp = requests.post(
"http://motoapi.amazonaws.com:5000/moto-api/static/rds-data/statement-results",
json=expected_results,
)
resp.status_code.should.equal(201)
rdsdata = boto3.client("rds-data", region_name="us-east-1")
resp = rdsdata.execute_statement(resourceArn="not applicable", secretArn="not applicable", sql="SELECT some FROM thing")
"""
if (resource_arn, sql) in self.sql_results:
return self.sql_results[(resource_arn, sql)]
elif self.results_queue:
self.sql_results[(resource_arn, sql)] = self.results_queue.pop()
return self.sql_results[(resource_arn, sql)]
else:
return QueryResults(records=[])
rdsdata_backends = BackendDict(RDSDataServiceBackend, "rds-data")

22
moto/rdsdata/responses.py Normal file
View File

@ -0,0 +1,22 @@
import json
from moto.core.responses import BaseResponse
from .models import rdsdata_backends, RDSDataServiceBackend
class RDSDataServiceResponse(BaseResponse):
def __init__(self) -> None:
super().__init__(service_name="rds-data")
@property
def rdsdata_backend(self) -> RDSDataServiceBackend:
"""Return backend instance specific for this region."""
return rdsdata_backends[self.current_account][self.region]
def execute_statement(self) -> str:
resource_arn = self._get_param("resourceArn")
sql = self._get_param("sql")
query_result = self.rdsdata_backend.execute_statement(
resource_arn=resource_arn, sql=sql
)
return json.dumps(query_result.to_json())

14
moto/rdsdata/urls.py Normal file
View File

@ -0,0 +1,14 @@
"""rdsdata base URL and path."""
from .responses import RDSDataServiceResponse
url_bases = [
r"https?://rds-data\.(.+)\.amazonaws\.com",
]
response = RDSDataServiceResponse()
url_paths = {
"{0}/Execute$": response.dispatch,
}

View File

@ -235,7 +235,7 @@ disable = W,C,R,E
enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
[mypy]
files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/managedblockchain,moto/moto_api,moto/neptune,moto/opensearch
files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/managedblockchain,moto/moto_api,moto/neptune,moto/opensearch,moto/rdsdata
show_column_numbers=True
show_error_codes = True
disable_error_code=abstract

View File

View File

@ -0,0 +1,54 @@
import boto3
import requests
from moto import mock_rdsdata, settings
# See our Development Tips on writing tests for hints on how to write good tests:
# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
@mock_rdsdata
def test_execute_statement():
rdsdata = boto3.client("rds-data", region_name="eu-west-1")
resp = rdsdata.execute_statement(
resourceArn="not applicable",
secretArn="not applicable",
sql="SELECT some FROM thing",
)
assert resp["records"] == []
@mock_rdsdata
def test_set_query_results():
base_url = (
"localhost:5000" if settings.TEST_SERVER_MODE else "motoapi.amazonaws.com"
)
sql_result = {
"results": [
{
"records": [[{"isNull": True}], [{"isNull": False}]],
"columnMetadata": [{"name": "a"}],
"formattedRecords": "some json",
}
],
"region": "us-west-1",
}
resp = requests.post(
f"http://{base_url}/moto-api/static/rds-data/statement-results",
json=sql_result,
)
assert resp.status_code == 201
rdsdata = boto3.client("rds-data", region_name="us-west-1")
resp = rdsdata.execute_statement(
resourceArn="not applicable",
secretArn="not applicable",
sql="SELECT some FROM thing",
)
assert resp["records"] == [[{"isNull": True}], [{"isNull": False}]]
assert resp["formattedRecords"] == "some json"