Repository: great-expectations/great_expectations
Pull request #11119: [FEATURE] Changes to Redshift `connection_string` validator to support a dict type
Author: login: alena-hutchinson
Created: 2025-04-22T23:02:23Z | Merged: 2025-04-24T00:50:44Z

Updates to the `RedshiftDatasource` validator to support when the `connection_string` is a `dict` type. 

- [ ] Description of PR changes above includes a link to [an existing GitHub issue](https://github.com/great-expectations/great_expectations/issues)
- [ ] PR title is prefixed with one of: [BUGFIX], [FEATURE], [DOCS], [MAINTENANCE], [CONTRIB], [MINORBUMP]
- [ ] Code is linted - run `invoke lint` (uses `ruff format` + `ruff check`)
- [ ] Appropriate tests and docs have been updated

For more information about contributing, visit our [community resources](https://docs.greatexpectations.io/docs/core/introduction/community_resources#contribute-code-or-documentation).

After you submit your PR, keep the page open and **monitor the statuses of the various checks made by our continuous integration process at the bottom of the page. Please fix any issues that come up** and [reach out on Slack](https://greatexpectations.io/slack) if you need help. Thanks for contributing!



Diff:
diff --git a/great_expectations/datasource/fluent/redshift_datasource.py b/great_expectations/datasource/fluent/redshift_datasource.py
index 9a2150097b5a..fe2bb3cf8f2c 100644
--- a/great_expectations/datasource/fluent/redshift_datasource.py
+++ b/great_expectations/datasource/fluent/redshift_datasource.py
@@ -6,9 +6,7 @@
 from great_expectations.compatibility.pydantic import (
     AnyUrl,
     BaseModel,
-    StrictInt,
-    StrictStr,
-    root_validator,
+    validator,
 )
 from great_expectations.compatibility.typing_extensions import override
 from great_expectations.datasource.fluent.config_str import ConfigStr
@@ -39,15 +37,14 @@ class RedshiftSSLModes(Enum):
 class RedshiftConnectionDetails(BaseModel):
     """
     Information needed to connect to a Redshift database.
-    Alternative to a connection string.
     """
 
-    username: StrictStr
-    password: Union[ConfigStr, StrictStr]
-    host: StrictStr
-    port: StrictInt
-    database: StrictStr
-    ssl_mode: RedshiftSSLModes
+    user: str
+    password: Union[ConfigStr, str]
+    host: str
+    port: int
+    database: str
+    sslmode: RedshiftSSLModes
 
 
 class RedshiftDatasource(SQLDatasource):
@@ -55,34 +52,36 @@ class RedshiftDatasource(SQLDatasource):
 
     Args:
         name: The name of this Redshift datasource.
-        connection_string: The SQLAlchemy connection string used to connect to the Redshift
-            database. This will use a redshift with psycopg2. For example:
-            "redshift+psycopg2://username@host.amazonaws.com:5439/database"
-        connection_details: A RedshiftConnectionDetails object defining the connection details.
+        connection_string: The SQLAlchemy connection string used to connect to the Redshift database
+            For example:
+            "redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode".
         If connection_details is used, connection_string cannot also be provided.
         assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose
             values are TableAsset or QueryAsset objects.
     """
 
     type: Literal["redshift"] = "redshift"  # type: ignore[assignment] # This is a hardcoded constant
-    connection_string: Union[ConfigStr, RedshiftDsn]
+    connection_string: Union[ConfigStr, dict, RedshiftConnectionDetails, RedshiftDsn]  # type: ignore[assignment] # Deviation from parent class as individual args are supported for connection
 
-    @root_validator(pre=True, allow_reuse=True)
-    def _build_connection_string_from_connection_details(cls, values: dict) -> dict:
+    @validator("connection_string", pre=True)
+    def _build_connection_string_from_connection_details(
+        cls, connection_string: dict | RedshiftConnectionDetails | str
+    ) -> str:
         """
-        If connection_details is provided, use them to construct the connection_string.
+        If a dict or RedshiftConnectionDetails object is provided, construct the formatted
+        connection string.
         """
-        connection_string, connection_details = (
-            values.get("connection_string"),
-            values.get("connection_details"),
-        )
-        if connection_details is not None and connection_string is not None:
-            raise ValueError("Cannot provide both connection_details and connection_string")  # noqa: TRY003
-        if connection_details is not None:
-            connection_string_from_details = f"redshift+psycopg2://{connection_details.username}:{connection_details.password}@{connection_details.host}:{connection_details.port}/{connection_details.database}?sslmode={connection_details.ssl_mode.value}"
-            values["connection_string"] = connection_string_from_details
-            del values["connection_details"]
-        return values
+        if isinstance(connection_string, str):
+            return connection_string
+
+        if isinstance(connection_string, dict):
+            connection_details = RedshiftConnectionDetails(**connection_string)
+        elif isinstance(connection_string, RedshiftConnectionDetails):
+            connection_details = connection_string
+        else:
+            raise TypeError("Invalid connection_string type: ", type(connection_string))  # noqa: TRY003
+        connection_string = f"redshift+psycopg2://{connection_details.user}:{connection_details.password}@{connection_details.host}:{connection_details.port}/{connection_details.database}?sslmode={connection_details.sslmode.value}"
+        return connection_string
 
     @property
     @override
diff --git a/great_expectations/datasource/fluent/schemas/RedshiftDatasource.json b/great_expectations/datasource/fluent/schemas/RedshiftDatasource.json
index 5769605d0515..377f887b69ce 100644
--- a/great_expectations/datasource/fluent/schemas/RedshiftDatasource.json
+++ b/great_expectations/datasource/fluent/schemas/RedshiftDatasource.json
@@ -1,6 +1,6 @@
 {
     "title": "RedshiftDatasource",
-    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://username@host.amazonaws.com:5439/database\"\n    connection_details: A RedshiftConnectionDetails object defining the connection details.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
+    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift database\n        For example:\n        \"redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode\".\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
     "type": "object",
     "properties": {
         "type": {
@@ -57,6 +57,12 @@
                         "${MY_CFG_VAR}"
                     ]
                 },
+                {
+                    "type": "object"
+                },
+                {
+                    "$ref": "#/definitions/RedshiftConnectionDetails"
+                },
                 {
                     "type": "string",
                     "minLength": 1,
@@ -535,6 +541,71 @@
                 "query"
             ],
             "additionalProperties": false
+        },
+        "RedshiftSSLModes": {
+            "title": "RedshiftSSLModes",
+            "description": "An enumeration.",
+            "enum": [
+                "disable",
+                "allow",
+                "prefer",
+                "require",
+                "verify-ca",
+                "verify-full"
+            ]
+        },
+        "RedshiftConnectionDetails": {
+            "title": "RedshiftConnectionDetails",
+            "description": "Information needed to connect to a Redshift database.",
+            "type": "object",
+            "properties": {
+                "user": {
+                    "title": "User",
+                    "type": "string"
+                },
+                "password": {
+                    "title": "Password",
+                    "anyOf": [
+                        {
+                            "type": "string",
+                            "writeOnly": true,
+                            "format": "password",
+                            "description": "Contains config templates to be substituted at runtime. Runtime values will never be serialized.",
+                            "pattern": ".*(?<!\\\\)\\$\\{(.*?)\\}|(?<!\\\\)\\$([_a-zA-Z][_a-zA-Z0-9]*).*",
+                            "examples": [
+                                "hello_${NAME}",
+                                "${MY_CFG_VAR}"
+                            ]
+                        },
+                        {
+                            "type": "string"
+                        }
+                    ]
+                },
+                "host": {
+                    "title": "Host",
+                    "type": "string"
+                },
+                "port": {
+                    "title": "Port",
+                    "type": "integer"
+                },
+                "database": {
+                    "title": "Database",
+                    "type": "string"
+                },
+                "sslmode": {
+                    "$ref": "#/definitions/RedshiftSSLModes"
+                }
+            },
+            "required": [
+                "user",
+                "password",
+                "host",
+                "port",
+                "database",
+                "sslmode"
+            ]
         }
     }
 }
diff --git a/tests/datasource/fluent/test_redshift_datasource.py b/tests/datasource/fluent/test_redshift_datasource.py
index 470ed4ba55fe..8bacadbd390e 100644
--- a/tests/datasource/fluent/test_redshift_datasource.py
+++ b/tests/datasource/fluent/test_redshift_datasource.py
@@ -13,50 +13,49 @@
 LOGGER = logging.getLogger(__name__)
 
 
-def build_connection_string(scheme, username, password, host, port, database, ssl_mode):
-    return f"{scheme}://{username}:{password}@{host}:{port}/{database}?sslmode={ssl_mode}"
-
-
 @pytest.fixture
 def scheme():
     return "redshift+psycopg2"
 
 
 @pytest.mark.unit
-def test_create_engine_is_called_with_expected_kwargs_using_connection_string(
-    sa,
-    mocker: MockerFixture,
-    ephemeral_context_with_defaults: EphemeralDataContext,
-    scheme,
-):
-    create_engine_spy = mocker.patch.object(sa, "create_engine")
-
-    username = "username"
-    password = "password"
-    host = "host"
-    port = 1234
-    database = "database"
-    ssl_mode = "allow"
-
-    context = ephemeral_context_with_defaults
-    connection_string = build_connection_string(
-        scheme, username, password, host, port, database, ssl_mode
-    )
-    data_source = context.data_sources.add_redshift(
-        name="redshift_test", connection_string=connection_string
-    )
-    data_source.get_engine()  # we will verify that the correct connection details are used when getting the engine  # noqa: E501
-
-    expected_kwargs = RedshiftDsn(
-        connection_string,
-        scheme=scheme,
-    )
-
-    create_engine_spy.assert_called_once_with(expected_kwargs)
-
-
-@pytest.mark.unit
-def test_create_engine_is_called_with_expected_kwargs_using_connection_details(
+@pytest.mark.parametrize(
+    "connection_input,expected_connection_string",
+    [
+        pytest.param(
+            "redshift+psycopg2://user:password@host:1234/database?sslmode=allow",
+            "redshift+psycopg2://user:password@host:1234/database?sslmode=allow",
+            id="string type",
+        ),
+        pytest.param(
+            {
+                "user": "user",
+                "password": "password",
+                "host": "host",
+                "port": 1234,
+                "database": "database",
+                "sslmode": RedshiftSSLModes.ALLOW,
+            },
+            "redshift+psycopg2://user:password@host:1234/database?sslmode=allow",
+            id="dict type",
+        ),
+        pytest.param(
+            RedshiftConnectionDetails(
+                user="user",
+                password="password",
+                host="host",
+                port=1234,
+                database="database",
+                sslmode=RedshiftSSLModes.ALLOW,
+            ),
+            "redshift+psycopg2://user:password@host:1234/database?sslmode=allow",
+            id="dict type",
+        ),
+    ],
+)
+def test_create_engine_is_called_with_expected_kwargs(
+    connection_input,
+    expected_connection_string,
     sa,
     mocker: MockerFixture,
     ephemeral_context_with_defaults: EphemeralDataContext,
@@ -64,33 +63,15 @@ def test_create_engine_is_called_with_expected_kwargs_using_connection_details(
 ):
     create_engine_spy = mocker.patch.object(sa, "create_engine")
 
-    username = "username"
-    password = "password"
-    host = "host"
-    port = 1234
-    database = "database"
-    ssl_mode = RedshiftSSLModes.ALLOW
-    connection_details = RedshiftConnectionDetails(
-        username=username,
-        password=password,
-        host=host,
-        port=port,
-        database=database,
-        ssl_mode=ssl_mode,
-    )
-
     context = ephemeral_context_with_defaults
     data_source = context.data_sources.add_redshift(
         name="redshift_test",
-        connection_details=connection_details,  # type: ignore[call-arg]
+        connection_string=connection_input,
     )
     data_source.get_engine()  # we will verify that the correct connection details are used when getting the engine  # noqa: E501
 
-    connection_string = build_connection_string(
-        scheme, username, password, host, port, database, ssl_mode.value
-    )
     expected_kwargs = RedshiftDsn(
-        connection_string,
+        expected_connection_string,
         scheme=scheme,
     )
 
@@ -103,19 +84,19 @@ def test_value_error_raised_if_invalid_connection_detail_inputs(
     ephemeral_context_with_defaults: EphemeralDataContext,
     scheme,
 ):
-    username = "username"
+    user = "user"
     password = "password"
     host = "host"
     port = 1234
     database = "database"
-    ssl_mode = "INVALID"
+    sslmode = "INVALID"
 
     with pytest.raises(ValueError):
         RedshiftConnectionDetails(
-            username=username,
+            user=user,
             password=password,
             host=host,
             port=port,
             database=database,
-            ssl_mode=ssl_mode,  # type: ignore[arg-type] # Ignore this for purpose of the test
+            sslmode=sslmode,  # type: ignore[arg-type] # Ignore this for purpose of the test
         )


Reviews:
user: login: klavavej
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T16:46:31Z
user: login: billdirks
body: Thanks for doing this! I had a few small comments. The main one is adding a dict type as a connection_string parameter since it looks like we take that and need to for the agent. Also adding a test for `dict` input would be good.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T17:21:05Z
user: login: alena-hutchinson
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T17:51:33Z
user: login: billdirks
body: LGTM, thanks!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T21:08:11Z

Review comments:
diff_hunk: @@ -1,6 +1,6 @@
 {
     "title": "RedshiftDatasource",
-    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://username@host.amazonaws.com:5439/database\"\n    connection_details: A RedshiftConnectionDetails object defining the connection details.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
+    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode\".\n        Alternatively, a RedshiftConnectionDetails object can be used.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
path: great_expectations/datasource/fluent/schemas/RedshiftDatasource.json
user: login: klavavej
body: I don't think this is the right place to apply this change (this looks like generated json) but there are some stray words in this description compared to the similar page for [postgres](https://deploy-preview-11097.docs.greatexpectations.io/docs/reference/api/datasource/fluent/postgresdatasource_class/). This sentence also has grammar issues so I suggest removing it.

```suggestion
    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. For example:\n        \"redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode\".\n        Alternatively, a RedshiftConnectionDetails object can be used.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
```
created_at: 2025-04-23T16:46:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,32 +55,34 @@ class RedshiftDatasource(SQLDatasource):
         name: The name of this Redshift datasource.
         connection_string: The SQLAlchemy connection string used to connect to the Redshift
             database. This will use a redshift with psycopg2. For example:
-            "redshift+psycopg2://username@host.amazonaws.com:5439/database"
-        connection_details: A RedshiftConnectionDetails object defining the connection details.
+            "redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode".
+            Alternatively, a RedshiftConnectionDetails object can be used.
         If connection_details is used, connection_string cannot also be provided.
path: great_expectations/datasource/fluent/redshift_datasource.py
user: login: billdirks
body: You can remove this line since there is no more `connection_details` argument. It looks like we also accept a dictionary whose keys are the connection detail attributes.
created_at: 2025-04-23T17:03:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,32 +55,34 @@ class RedshiftDatasource(SQLDatasource):
         name: The name of this Redshift datasource.
         connection_string: The SQLAlchemy connection string used to connect to the Redshift
             database. This will use a redshift with psycopg2. For example:
-            "redshift+psycopg2://username@host.amazonaws.com:5439/database"
-        connection_details: A RedshiftConnectionDetails object defining the connection details.
+            "redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode".
+            Alternatively, a RedshiftConnectionDetails object can be used.
         If connection_details is used, connection_string cannot also be provided.
         assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose
             values are TableAsset or QueryAsset objects.
     """
 
     type: Literal["redshift"] = "redshift"  # type: ignore[assignment] # This is a hardcoded constant
-    connection_string: Union[ConfigStr, RedshiftDsn]
+    connection_string: Union[RedshiftConnectionDetails, ConfigStr, RedshiftDsn]  # type: ignore[assignment] # Deviation from parent class as individual args are supported for connection
path: great_expectations/datasource/fluent/redshift_datasource.py
user: login: billdirks
body: I think you can remove the `RedshiftConnectionDetails` since you always coerce to a `str`.
Pros: Someone who goes `redshift_datasource.connection_string` doesn't have to handle the different union types.
Cons: Autocomplete will only give the string argument. One will have to do type ignores when passing in these non-str types.
We don't annotate the `dict` argument.

I've gone back and forth. I think we can leave this with the type union. I'd add the dict type, probably as a TypedDict with the right keys.
created_at: 2025-04-23T17:11:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,6 @@
 {
     "title": "RedshiftDatasource",
-    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://username@host.amazonaws.com:5439/database\"\n    connection_details: A RedshiftConnectionDetails object defining the connection details.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
+    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode\".\n        Alternatively, a RedshiftConnectionDetails object can be used.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
path: great_expectations/datasource/fluent/schemas/RedshiftDatasource.json
user: login: billdirks
body: This is derived from the docstring in `redshift_datasource.py`.
created_at: 2025-04-23T17:12:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -56,38 +56,38 @@ def test_create_engine_is_called_with_expected_kwargs_using_connection_string(
 
 
 @pytest.mark.unit
-def test_create_engine_is_called_with_expected_kwargs_using_connection_details(
+def test_create_engine_is_called_with_expected_kwargs_using_connection_string_object_type(
     sa,
     mocker: MockerFixture,
     ephemeral_context_with_defaults: EphemeralDataContext,
     scheme,
 ):
     create_engine_spy = mocker.patch.object(sa, "create_engine")
 
-    username = "username"
+    user = "user"
     password = "password"
     host = "host"
     port = 1234
     database = "database"
-    ssl_mode = RedshiftSSLModes.ALLOW
+    sslmode = RedshiftSSLModes.ALLOW
     connection_details = RedshiftConnectionDetails(
path: tests/datasource/fluent/test_redshift_datasource.py
user: login: billdirks
body: It would be good to add 1 more test identical to this one where we use a dict instead.
You might be able to write a parameterized test with the inputs the connection_string argument and the expected arguments you assert against.
created_at: 2025-04-23T17:16:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,6 @@
 {
     "title": "RedshiftDatasource",
-    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://username@host.amazonaws.com:5439/database\"\n    connection_details: A RedshiftConnectionDetails object defining the connection details.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
+    "description": "Adds a Redshift datasource to the data context using psycopg2.\n\nArgs:\n    name: The name of this Redshift datasource.\n    connection_string: The SQLAlchemy connection string used to connect to the Redshift\n        database. This will use a redshift with psycopg2. For example:\n        \"redshift+psycopg2://user:password@host.amazonaws.com:5439/database?sslmode=sslmode\".\n        Alternatively, a RedshiftConnectionDetails object can be used.\n    If connection_details is used, connection_string cannot also be provided.\n    assets: An optional dictionary whose keys are TableAsset or QueryAsset names and whose\n        values are TableAsset or QueryAsset objects.",
path: great_expectations/datasource/fluent/schemas/RedshiftDatasource.json
user: login: alena-hutchinson
body: Removed from `redshift_datasource.py` and regenerated schema
created_at: 2025-04-23T17:51:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: netlify[bot]
created_at: 2025-04-22T23:02:44Z
body: ### <span aria-hidden="true">✅</span> Deploy Preview for *niobium-lead-7998* canceled.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | a234dfdb7754dac8f5adf9dd380c9e08ad27edec |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/niobium-lead-7998/deploys/680953f6d377240008e3451a |
author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-22T23:04:54Z
body: ## [Codecov](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) Report
Attention: Patch coverage is `94.44444%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 81.18%. Comparing base [(`8c98d08`)](https://app.codecov.io/gh/great-expectations/great_expectations/commit/8c98d08478f9cfb65a41a927beade211abf972c4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) to head [(`a234dfd`)](https://app.codecov.io/gh/great-expectations/great_expectations/commit/a234dfdb7754dac8f5adf9dd380c9e08ad27edec?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations).
> Report is 2 commits behind head on develop.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | Patch % | Lines |
|---|---|---|
| [...ectations/datasource/fluent/redshift\_datasource.py](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119?src=pr&el=tree&filepath=great_expectations%2Fdatasource%2Ffluent%2Fredshift_datasource.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations#diff-Z3JlYXRfZXhwZWN0YXRpb25zL2RhdGFzb3VyY2UvZmx1ZW50L3JlZHNoaWZ0X2RhdGFzb3VyY2UucHk=) | 94.44% | [1 Missing :warning: ](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop   #11119      +/-   ##
===========================================
+ Coverage    81.17%   81.18%   +0.01%     
===========================================
  Files          502      502              
  Lines        41454    41455       +1     
===========================================
+ Hits         33652    33657       +5     
+ Misses        7802     7798       -4     
```

| [Flag](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | Coverage Δ | |
|---|---|---|
| [3.10](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `70.46% <94.44%> (-0.02%)` | :arrow_down: |
| [3.10 athena or openpyxl or pyarrow or project or sqlite or aws_creds](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 aws_deps](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 big](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 clickhouse](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 filesystem](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 mssql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 mysql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 postgresql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.10 spark_connect](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `70.47% <94.44%> (+0.01%)` | :arrow_up: |
| [3.11 athena or openpyxl or pyarrow or project or sqlite or aws_creds](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 aws_deps](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 big](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 clickhouse](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 filesystem](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 mssql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 mysql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.11 spark_connect](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `?` | |
| [3.12](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `70.48% <94.44%> (+<0.01%)` | :arrow_up: |
| [3.12 athena or openpyxl or pyarrow or project or sqlite or aws_creds](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `57.11% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 aws_deps](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `46.86% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 big](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `55.19% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 bigquery](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `49.60% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 databricks](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `51.22% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 filesystem](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `63.28% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 gx-redshift](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `48.81% <61.11%> (+0.01%)` | :arrow_up: |
| [3.12 mssql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.10% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 mysql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.35% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 postgresql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `55.17% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 redshift](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `48.78% <61.11%> (+<0.01%)` | :arrow_up: |
| [3.12 snowflake](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `51.99% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 spark](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `58.28% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 spark_connect](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `47.18% <50.00%> (-0.01%)` | :arrow_down: |
| [3.12 trino](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.77% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `70.50% <94.44%> (+<0.01%)` | :arrow_up: |
| [3.9 athena or openpyxl or pyarrow or project or sqlite or aws_creds](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `57.11% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 aws_deps](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `46.88% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 big](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `55.20% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 bigquery](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `49.60% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 clickhouse](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `43.82% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 databricks](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `51.22% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 filesystem](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `63.28% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 gx-redshift](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `48.81% <61.11%> (+0.01%)` | :arrow_up: |
| [3.9 mssql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.09% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 mysql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.33% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 postgresql](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `55.15% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 redshift](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `48.76% <61.11%> (-0.01%)` | :arrow_down: |
| [3.9 snowflake](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `51.99% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 spark](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `58.25% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 spark_connect](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `47.19% <50.00%> (-0.01%)` | :arrow_down: |
| [3.9 trino](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.76% <50.00%> (-0.01%)` | :arrow_down: |
| [cloud](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `0.00% <0.00%> (ø)` | |
| [docs-basic](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `54.33% <50.00%> (-0.01%)` | :arrow_down: |
| [docs-creds-needed](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `53.25% <61.11%> (+0.04%)` | :arrow_up: |
| [docs-spark](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations) | `52.77% <50.00%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/great-expectations/great_expectations/pull/11119?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=great-expectations).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE