From 9c54280d8520f81a7928150194c4fb6c7b04e324 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Wed, 9 Aug 2023 16:42:14 -0700 Subject: [PATCH] feat: add MotherDuck DB engine spec (#24934) --- setup.py | 1 + .../DatabaseModal/SqlAlchemyForm.tsx | 25 ++++++++----------- .../databases/DatabaseModal/index.tsx | 11 ++++++-- .../src/features/databases/types.ts | 1 + superset/databases/api.py | 4 +++ superset/db_engine_specs/base.py | 11 ++++---- superset/db_engine_specs/duckdb.py | 7 ++++++ .../integration_tests/databases/api_tests.py | 3 +++ 8 files changed, 42 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index b3f225bce50f6..24b1b890d5565 100644 --- a/setup.py +++ b/setup.py @@ -150,6 +150,7 @@ def get_git_sha() -> str: "dremio": ["sqlalchemy-dremio>=1.1.5, <1.3"], "drill": ["sqlalchemy-drill==0.1.dev"], "druid": ["pydruid>=0.6.5,<0.7"], + "duckdb": ["duckdb-engine==0.8.1"], "dynamodb": ["pydynamodb>=0.4.2"], "solr": ["sqlalchemy-solr >= 0.2.0"], "elasticsearch": ["elasticsearch-dbapi>=0.2.9, <0.3.0"], diff --git a/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx b/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx index 5d50625cddcae..0dd4cd97d0967 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/SqlAlchemyForm.tsx @@ -38,17 +38,13 @@ const SqlAlchemyTab = ({ testInProgress?: boolean; children?: ReactNode; }) => { - let fallbackDocsUrl; - let fallbackDisplayText; - if (SupersetText) { - fallbackDocsUrl = - SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DOCS_URL; - fallbackDisplayText = - SupersetText.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DISPLAY_TEXT; - } else { - fallbackDocsUrl = 'https://docs.sqlalchemy.org/en/13/core/engines.html'; - fallbackDisplayText = 'SQLAlchemy docs'; - } + const fallbackDocsUrl = + SupersetText?.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DOCS_URL || + 'https://docs.sqlalchemy.org/en/13/core/engines.html'; + const fallbackDisplayText = + SupersetText?.DB_MODAL_SQLALCHEMY_FORM?.SQLALCHEMY_DISPLAY_TEXT || + 'SQLAlchemy docs'; + return ( <> @@ -82,9 +78,10 @@ const SqlAlchemyTab = ({ data-test="sqlalchemy-uri-input" value={db?.sqlalchemy_uri || ''} autoComplete="off" - placeholder={t( - 'dialect+driver://username:password@host:port/database', - )} + placeholder={ + db?.sqlalchemy_uri_placeholder || + t('dialect+driver://username:password@host:port/database') + } onChange={onInputChange} /> diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx index dd2e405350056..68dcfd4fed18c 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx @@ -202,6 +202,7 @@ export type DBReducerActionType = configuration_method: CONFIGURATION_METHOD; engine_information?: {}; driver?: string; + sqlalchemy_uri_placeholder?: string; }; } | { @@ -946,8 +947,13 @@ const DatabaseModal: FunctionComponent = ({ const selectedDbModel = availableDbs?.databases.filter( (db: DatabaseObject) => db.name === database_name, )[0]; - const { engine, parameters, engine_information, default_driver } = - selectedDbModel; + const { + engine, + parameters, + engine_information, + default_driver, + sqlalchemy_uri_placeholder, + } = selectedDbModel; const isDynamic = parameters !== undefined; setDB({ type: ActionType.dbSelected, @@ -959,6 +965,7 @@ const DatabaseModal: FunctionComponent = ({ : CONFIGURATION_METHOD.SQLALCHEMY_URI, engine_information, driver: default_driver, + sqlalchemy_uri_placeholder, }, }); diff --git a/superset-frontend/src/features/databases/types.ts b/superset-frontend/src/features/databases/types.ts index a7e4f59b581b9..e7089425bcfed 100644 --- a/superset-frontend/src/features/databases/types.ts +++ b/superset-frontend/src/features/databases/types.ts @@ -52,6 +52,7 @@ export type DatabaseObject = { name: string; // synonym to database_name paramProperties?: Record; sqlalchemy_uri?: string; + sqlalchemy_uri_placeholder?: string; parameters?: { access_token?: string; database_name?: string; diff --git a/superset/databases/api.py b/superset/databases/api.py index ae9cd60188950..116e2ddb1fa88 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -1293,6 +1293,9 @@ def available(self) -> Response: type: array items: type: string + sqlalchemy_uri_placeholder: + description: Placeholder for the SQLAlchemy URI + type: string default_driver: description: Default driver for the engine type: string @@ -1330,6 +1333,7 @@ def available(self) -> Response: "name": engine_spec.engine_name, "engine": engine_spec.engine, "available_drivers": sorted(drivers), + "sqlalchemy_uri_placeholder": engine_spec.sqlalchemy_uri_placeholder, "preferred": engine_spec.engine_name in preferred_databases, "engine_information": engine_spec.get_public_information(), } diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 2e1d5598ff35f..d3ffce018a7e1 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -185,6 +185,12 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods engine_aliases: set[str] = set() drivers: dict[str, str] = {} default_driver: str | None = None + + # placeholder with the SQLAlchemy URI template + sqlalchemy_uri_placeholder = ( + "engine+driver://user:password@host:port/dbname[?key=value&key=value...]" + ) + disable_ssh_tunneling = False _date_trunc_functions: dict[str, str] = {} @@ -1958,11 +1964,6 @@ class BasicParametersMixin: # recommended driver name for the DB engine spec default_driver = "" - # placeholder with the SQLAlchemy URI template - sqlalchemy_uri_placeholder = ( - "engine+driver://user:password@host:port/dbname[?key=value&key=value...]" - ) - # query parameter to enable encryption in the database connection # for Postgres this would be `{"sslmode": "verify-ca"}`, eg. encryption_parameters: dict[str, str] = {} diff --git a/superset/db_engine_specs/duckdb.py b/superset/db_engine_specs/duckdb.py index fa2f01f50a516..291b5521ef6a4 100644 --- a/superset/db_engine_specs/duckdb.py +++ b/superset/db_engine_specs/duckdb.py @@ -80,3 +80,10 @@ def get_table_names( cls, database: Database, inspector: Inspector, schema: str | None ) -> set[str]: return set(inspector.get_table_names(schema)) + + +class MotherDuckEngineSpec(DuckDBEngineSpec): + engine = "duckdb" + engine_name = "MotherDuck" + + sqlalchemy_uri_placeholder = "duckdb:///md:{SERVICE_TOKEN}@{database_name}" diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index 8bf4867d019d9..4709a11377497 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -3217,6 +3217,7 @@ def test_available(self, app, get_available_engine_specs): "engine": "hana", "name": "SAP HANA", "preferred": False, + "sqlalchemy_uri_placeholder": "engine+driver://user:password@host:port/dbname[?key=value&key=value...]", "engine_information": { "supports_file_upload": True, "disable_ssh_tunneling": False, @@ -3248,6 +3249,7 @@ def test_available_no_default(self, app, get_available_engine_specs): "engine": "mysql", "name": "MySQL", "preferred": True, + "sqlalchemy_uri_placeholder": "mysql://user:password@host:port/dbname[?key=value&key=value...]", "engine_information": { "supports_file_upload": True, "disable_ssh_tunneling": False, @@ -3258,6 +3260,7 @@ def test_available_no_default(self, app, get_available_engine_specs): "engine": "hana", "name": "SAP HANA", "preferred": False, + "sqlalchemy_uri_placeholder": "engine+driver://user:password@host:port/dbname[?key=value&key=value...]", "engine_information": { "supports_file_upload": True, "disable_ssh_tunneling": False,