diff --git a/superset/db_engine_specs/redshift.py b/superset/db_engine_specs/redshift.py index f2d2652089ee5..beff1f694b837 100644 --- a/superset/db_engine_specs/redshift.py +++ b/superset/db_engine_specs/redshift.py @@ -19,6 +19,7 @@ from flask_babel import gettext as __ +from superset.db_engine_specs.base import BasicParametersMixin from superset.db_engine_specs.postgres import PostgresBaseEngineSpec from superset.errors import SupersetErrorType @@ -45,26 +46,32 @@ ) -class RedshiftEngineSpec(PostgresBaseEngineSpec): +class RedshiftEngineSpec(PostgresBaseEngineSpec, BasicParametersMixin): engine = "redshift" engine_name = "Amazon Redshift" max_column_name_length = 127 + sqlalchemy_uri_placeholder = ( + "redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]" + ) + + encryption_parameters = {"sslmode": "verify-ca"} + custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType, Dict[str, Any]]] = { CONNECTION_ACCESS_DENIED_REGEX: ( __('Either the username "%(username)s" or the password is incorrect.'), SupersetErrorType.CONNECTION_ACCESS_DENIED_ERROR, - {}, + {"invalid": ["username", "password"]}, ), CONNECTION_INVALID_HOSTNAME_REGEX: ( __('The hostname "%(hostname)s" cannot be resolved.'), SupersetErrorType.CONNECTION_INVALID_HOSTNAME_ERROR, - {}, + {"invalid": ["host"]}, ), CONNECTION_PORT_CLOSED_REGEX: ( __('Port %(port)s on hostname "%(hostname)s" refused the connection.'), SupersetErrorType.CONNECTION_PORT_CLOSED_ERROR, - {}, + {"invalid": ["host", "port"]}, ), CONNECTION_HOST_DOWN_REGEX: ( __( @@ -72,7 +79,7 @@ class RedshiftEngineSpec(PostgresBaseEngineSpec): "reached on port %(port)s." ), SupersetErrorType.CONNECTION_HOST_DOWN_ERROR, - {}, + {"invalid": ["host", "port"]}, ), CONNECTION_UNKNOWN_DATABASE_REGEX: ( __( @@ -80,7 +87,7 @@ class RedshiftEngineSpec(PostgresBaseEngineSpec): " Please verify your database name and try again." ), SupersetErrorType.CONNECTION_UNKNOWN_DATABASE_ERROR, - {}, + {"invalid": ["database"]}, ), } diff --git a/tests/databases/api_tests.py b/tests/databases/api_tests.py index 1868f95d60fae..9ad0b1970e4c5 100644 --- a/tests/databases/api_tests.py +++ b/tests/databases/api_tests.py @@ -36,6 +36,7 @@ from superset.connectors.sqla.models import SqlaTable from superset.db_engine_specs.mysql import MySQLEngineSpec from superset.db_engine_specs.postgres import PostgresEngineSpec +from superset.db_engine_specs.redshift import RedshiftEngineSpec from superset.db_engine_specs.bigquery import BigQueryEngineSpec from superset.db_engine_specs.hana import HanaEngineSpec from superset.errors import SupersetError @@ -1371,10 +1372,14 @@ def test_function_names(self, mock_get_function_names): @mock.patch("superset.databases.api.get_available_engine_specs") @mock.patch("superset.databases.api.app") def test_available(self, app, get_available_engine_specs): - app.config = {"PREFERRED_DATABASES": ["postgresql"]} + app.config = { + "PREFERRED_DATABASES": ["postgresql", "biqquery", "mysql", "redshift"] + } get_available_engine_specs.return_value = [ PostgresEngineSpec, BigQueryEngineSpec, + MySQLEngineSpec, + RedshiftEngineSpec, HanaEngineSpec, ] @@ -1431,46 +1436,52 @@ def test_available(self, app, get_available_engine_specs): "sqlalchemy_uri_placeholder": "postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...]", }, { - "engine": "bigquery", - "name": "Google BigQuery", + "engine": "mysql", + "name": "MySQL", "parameters": { "properties": { - "credentials_info": { - "description": "Contents of BigQuery JSON credentials.", + "database": { + "description": "Database name", "type": "string", - "x-encrypted-extra": True, - } + }, + "encryption": { + "description": "Use an encrypted connection to the database", + "type": "boolean", + }, + "host": { + "description": "Hostname or IP address", + "type": "string", + }, + "password": { + "description": "Password", + "nullable": True, + "type": "string", + }, + "port": { + "description": "Database port", + "format": "int32", + "type": "integer", + }, + "query": { + "additionalProperties": {}, + "description": "Additional parameters", + "type": "object", + }, + "username": { + "description": "Username", + "nullable": True, + "type": "string", + }, }, + "required": ["database", "host", "port", "username"], "type": "object", }, - "preferred": False, - "sqlalchemy_uri_placeholder": "bigquery://{project_id}", + "preferred": True, + "sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]", }, - {"engine": "hana", "name": "SAP HANA", "preferred": False}, - ] - } - - @mock.patch("superset.databases.api.get_available_engine_specs") - @mock.patch("superset.databases.api.app") - def test_available_with_mysql(self, app, get_available_engine_specs): - app.config = {"PREFERRED_DATABASES": ["mysql"]} - get_available_engine_specs.return_value = [ - MySQLEngineSpec, - HanaEngineSpec, - ] - - self.login(username="admin") - uri = "api/v1/database/available/" - - rv = self.client.get(uri) - response = json.loads(rv.data.decode("utf-8")) - print(response) - assert rv.status_code == 200 - assert response == { - "databases": [ { - "engine": "mysql", - "name": "MySQL", + "engine": "redshift", + "name": "Amazon Redshift", "parameters": { "properties": { "database": { @@ -1510,7 +1521,23 @@ def test_available_with_mysql(self, app, get_available_engine_specs): "type": "object", }, "preferred": True, - "sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]", + "sqlalchemy_uri_placeholder": "redshift+psycopg2://user:password@host:port/dbname[?key=value&key=value...]", + }, + { + "engine": "bigquery", + "name": "Google BigQuery", + "parameters": { + "properties": { + "credentials_info": { + "description": "Contents of BigQuery JSON credentials.", + "type": "string", + "x-encrypted-extra": True, + } + }, + "type": "object", + }, + "preferred": False, + "sqlalchemy_uri_placeholder": "bigquery://{project_id}", }, {"engine": "hana", "name": "SAP HANA", "preferred": False}, ] diff --git a/tests/db_engine_specs/redshift_tests.py b/tests/db_engine_specs/redshift_tests.py index 483fbe54b27a2..49e34b1586448 100644 --- a/tests/db_engine_specs/redshift_tests.py +++ b/tests/db_engine_specs/redshift_tests.py @@ -34,6 +34,7 @@ def test_extract_errors(self): message='Either the username "wronguser" or the password is incorrect.', level=ErrorLevel.ERROR, extra={ + "invalid": ["username", "password"], "engine_name": "Amazon Redshift", "issue_codes": [ { @@ -62,6 +63,7 @@ def test_extract_errors(self): message='The hostname "badhost" cannot be resolved.', level=ErrorLevel.ERROR, extra={ + "invalid": ["host"], "engine_name": "Amazon Redshift", "issue_codes": [ { @@ -90,6 +92,7 @@ def test_extract_errors(self): message='Port 12345 on hostname "localhost" refused the connection.', level=ErrorLevel.ERROR, extra={ + "invalid": ["host", "port"], "engine_name": "Amazon Redshift", "issue_codes": [ {"code": 1008, "message": "Issue 1008 - The port is closed."} @@ -123,6 +126,7 @@ def test_extract_errors(self): "and can't be reached on the provided port.", } ], + "invalid": ["host", "port"], }, ) ] @@ -153,6 +157,7 @@ def test_extract_errors(self): "and can't be reached on the provided port.", } ], + "invalid": ["host", "port"], }, ) ] @@ -174,6 +179,7 @@ def test_extract_errors(self): "spelled incorrectly or does not exist.", } ], + "invalid": ["database"], }, ) ]