Feature: RDS Data (#6168)
This commit is contained in:
parent
02e892e30a
commit
52870d6114
@ -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>
|
@ -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)
|
||||
|
||||
|
74
docs/docs/services/rds-data.rst
Normal file
74
docs/docs/services/rds-data.rst
Normal 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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")),
|
||||
|
@ -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)
|
||||
|
@ -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, {}, ""
|
||||
|
@ -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
5
moto/rdsdata/__init__.py
Normal 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
85
moto/rdsdata/models.py
Normal 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
22
moto/rdsdata/responses.py
Normal 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
14
moto/rdsdata/urls.py
Normal 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,
|
||||
}
|
@ -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
|
||||
|
0
tests/test_rdsdata/__init__.py
Normal file
0
tests/test_rdsdata/__init__.py
Normal file
54
tests/test_rdsdata/test_rdsdata.py
Normal file
54
tests/test_rdsdata/test_rdsdata.py
Normal 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"
|
Loading…
Reference in New Issue
Block a user