diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index ccb36ef80..2b99d2f58 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -5233,6 +5233,18 @@
- [ ] switchover_read_replica
+## rds-data
+
+16% implemented
+
+- [ ] batch_execute_statement
+- [ ] begin_transaction
+- [ ] commit_transaction
+- [ ] execute_sql
+- [X] execute_statement
+- [ ] rollback_transaction
+
+
## redshift
25% implemented
@@ -6999,7 +7011,6 @@
- qldb
- qldb-session
- rbin
-- rds-data
- redshift-serverless
- resiliencehub
- resource-explorer-2
@@ -7056,4 +7067,4 @@
- workspaces
- workspaces-web
- xray
-
+
\ No newline at end of file
diff --git a/docs/docs/services/athena.rst b/docs/docs/services/athena.rst
index 98a29ff62..32c91addd 100644
--- a/docs/docs/services/athena.rst
+++ b/docs/docs/services/athena.rst
@@ -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)
diff --git a/docs/docs/services/rds-data.rst b/docs/docs/services/rds-data.rst
new file mode 100644
index 000000000..2358e6166
--- /dev/null
+++ b/docs/docs/services/rds-data.rst
@@ -0,0 +1,74 @@
+.. _implementedservice_rds-data:
+
+.. |start-h3| raw:: html
+
+
+
+.. |end-h3| raw:: html
+
+
+
+========
+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
+
diff --git a/moto/__init__.py b/moto/__init__.py
index 09db17d3f..b18f450ae 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -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"
diff --git a/moto/athena/models.py b/moto/athena/models.py
index 1b1936cc7..78eee9313 100644
--- a/moto/athena/models.py
+++ b/moto/athena/models.py
@@ -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)
diff --git a/moto/backend_index.py b/moto/backend_index.py
index bca7e62fa..dccc30d37 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -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")),
diff --git a/moto/moto_api/_internal/models.py b/moto/moto_api/_internal/models.py
index 373c00938..ab9d02914 100644
--- a/moto/moto_api/_internal/models.py
+++ b/moto/moto_api/_internal/models.py
@@ -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)
diff --git a/moto/moto_api/_internal/responses.py b/moto/moto_api/_internal/responses.py
index 03a0ac684..bcde07da2 100644
--- a/moto/moto_api/_internal/responses.py
+++ b/moto/moto_api/_internal/responses.py
@@ -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, {}, ""
diff --git a/moto/moto_api/_internal/urls.py b/moto/moto_api/_internal/urls.py
index 0d1dd46a2..41506174c 100644
--- a/moto/moto_api/_internal/urls.py
+++ b/moto/moto_api/_internal/urls.py
@@ -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,
diff --git a/moto/rdsdata/__init__.py b/moto/rdsdata/__init__.py
new file mode 100644
index 000000000..41ff3651f
--- /dev/null
+++ b/moto/rdsdata/__init__.py
@@ -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)
diff --git a/moto/rdsdata/models.py b/moto/rdsdata/models.py
new file mode 100644
index 000000000..bb85a6052
--- /dev/null
+++ b/moto/rdsdata/models.py
@@ -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")
diff --git a/moto/rdsdata/responses.py b/moto/rdsdata/responses.py
new file mode 100644
index 000000000..f3bd89cb0
--- /dev/null
+++ b/moto/rdsdata/responses.py
@@ -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())
diff --git a/moto/rdsdata/urls.py b/moto/rdsdata/urls.py
new file mode 100644
index 000000000..2e9f60128
--- /dev/null
+++ b/moto/rdsdata/urls.py
@@ -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,
+}
diff --git a/setup.cfg b/setup.cfg
index 48b11dbf4..e57eaf2c4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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
diff --git a/tests/test_rdsdata/__init__.py b/tests/test_rdsdata/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_rdsdata/test_rdsdata.py b/tests/test_rdsdata/test_rdsdata.py
new file mode 100644
index 000000000..b528de520
--- /dev/null
+++ b/tests/test_rdsdata/test_rdsdata.py
@@ -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"