From df690ea93967862dc1f7fd45e232be28158e4256 Mon Sep 17 00:00:00 2001 From: Will Lachance Date: Fri, 2 Jan 2026 16:39:57 -0500 Subject: [PATCH 1/2] duckdb: Show catalog (database) where applicable (e.g. Motherduck) --- redash/query_runner/duckdb.py | 23 +++++++++++--- tests/query_runner/test_duckdb.py | 51 +++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/redash/query_runner/duckdb.py b/redash/query_runner/duckdb.py index f1e7cfbc2b..676accdb43 100644 --- a/redash/query_runner/duckdb.py +++ b/redash/query_runner/duckdb.py @@ -112,7 +112,7 @@ def run_query(self, query, user) -> tuple: def get_schema(self, get_stats=False) -> list: tables_query = """ - SELECT table_schema, table_name FROM information_schema.tables + SELECT table_catalog, table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog'); """ tables_results, error = self.run_query(tables_query, None) @@ -121,13 +121,26 @@ def get_schema(self, get_stats=False) -> list: schema = {} for table_row in tables_results["rows"]: - full_table_name = f"{table_row['table_schema']}.{table_row['table_name']}" - schema[full_table_name] = {"name": full_table_name, "columns": []} + # Include catalog (database) in the full table name for MotherDuck support + catalog = table_row["table_catalog"] + schema_name = table_row["table_schema"] + table_name = table_row["table_name"] + + # Skip catalog prefix for default local databases (memory, temp) + # but include it for MotherDuck and attached databases + if catalog.lower() in ("memory", "temp", "system"): + full_table_name = f"{schema_name}.{table_name}" + describe_query = f'DESCRIBE "{schema_name}"."{table_name}";' + else: + full_table_name = f"{catalog}.{schema_name}.{table_name}" + describe_query = f'DESCRIBE "{catalog}"."{schema_name}"."{table_name}";' - describe_query = f'DESCRIBE "{table_row["table_schema"]}"."{table_row["table_name"]}";' + schema[full_table_name] = {"name": full_table_name, "columns": []} columns_results, error = self.run_query(describe_query, None) if error: - logger.warning("Failed to describe table %s: %s", full_table_name, error) + logger.warning( + "Failed to describe table %s: %s", full_table_name, error + ) continue for col_row in columns_results["rows"]: diff --git a/tests/query_runner/test_duckdb.py b/tests/query_runner/test_duckdb.py index 4ca4be1854..a292423d19 100644 --- a/tests/query_runner/test_duckdb.py +++ b/tests/query_runner/test_duckdb.py @@ -13,7 +13,15 @@ def test_simple_schema_build(self, mock_run_query) -> None: # Simulate queries: first for tables, then for DESCRIBE mock_run_query.side_effect = [ ( - {"rows": [{"table_schema": "main", "table_name": "users"}]}, + { + "rows": [ + { + "table_catalog": "memory", + "table_schema": "main", + "table_name": "users", + } + ] + }, None, ), ( @@ -40,7 +48,15 @@ def test_struct_column_expansion(self, mock_run_query) -> None: # First call to run_query -> tables list mock_run_query.side_effect = [ ( - {"rows": [{"table_schema": "main", "table_name": "events"}]}, + { + "rows": [ + { + "table_catalog": "memory", + "table_schema": "main", + "table_name": "events", + } + ] + }, None, ), # Second call -> DESCRIBE output @@ -99,6 +115,37 @@ def test_nested_struct_expansion(self) -> None: assert "info.tags.primary_tag" in colnames assert "info.tags.secondary_tag" in colnames + @patch.object(DuckDB, "run_query") + def test_motherduck_catalog_included(self, mock_run_query) -> None: + # Test that non-default catalogs (like MotherDuck) include catalog in name + mock_run_query.side_effect = [ + ( + { + "rows": [ + { + "table_catalog": "sample_data", + "table_schema": "kaggle", + "table_name": "movies", + } + ] + }, + None, + ), + ( + { + "rows": [ + {"column_name": "title", "column_type": "VARCHAR"}, + ] + }, + None, + ), + ] + + schema = self.runner.get_schema() + self.assertEqual(len(schema), 1) + # Should include catalog name for non-default catalogs + self.assertEqual(schema[0]["name"], "sample_data.kaggle.movies") + @patch.object(DuckDB, "run_query") def test_error_propagation(self, mock_run_query) -> None: mock_run_query.return_value = (None, "boom") From b5ad92154d56fb376cbd018fc6bad0413793733f Mon Sep 17 00:00:00 2001 From: Will Lachance Date: Fri, 2 Jan 2026 16:49:20 -0500 Subject: [PATCH 2/2] black --- redash/query_runner/duckdb.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/redash/query_runner/duckdb.py b/redash/query_runner/duckdb.py index 676accdb43..f271e48d13 100644 --- a/redash/query_runner/duckdb.py +++ b/redash/query_runner/duckdb.py @@ -67,7 +67,10 @@ def configuration_schema(cls): "title": "Database Path", "default": ":memory:", }, - "extensions": {"type": "string", "title": "Extensions (comma separated)"}, + "extensions": { + "type": "string", + "title": "Extensions (comma separated)", + }, }, "order": ["dbpath", "extensions"], "required": ["dbpath"], @@ -138,9 +141,7 @@ def get_schema(self, get_stats=False) -> list: schema[full_table_name] = {"name": full_table_name, "columns": []} columns_results, error = self.run_query(describe_query, None) if error: - logger.warning( - "Failed to describe table %s: %s", full_table_name, error - ) + logger.warning("Failed to describe table %s: %s", full_table_name, error) continue for col_row in columns_results["rows"]: