From 560b6cc2180264ffb894f8c3b6a7eb7f2c78ceaf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 13:20:48 +0000 Subject: [PATCH 1/3] fix: resolve refs from disabled manifest nodes when elementary_enabled=False corrupts manifest When a test runs dbt with elementary_enabled=False (e.g. test_collect_metrics_elementary_disabled), dbt rewrites target/manifest.json with elementary models in the 'disabled' section instead of 'nodes'. If a subsequent test on the same xdist worker uses AdapterQueryRunner to read_table, it reads this stale manifest and fails with 'Cannot resolve ref: not found in dbt manifest'. Fix: _load_manifest_maps now scans both 'nodes' and 'disabled' sections. For disabled nodes that lack relation_name (dbt skips compilation for disabled nodes), the relation name is synthesized from database/schema/alias using the adapter's Relation class. Co-Authored-By: Itamar Hartstein --- .../tests/adapter_query_runner.py | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/integration_tests/tests/adapter_query_runner.py b/integration_tests/tests/adapter_query_runner.py index 580d600cc..89f659cbf 100644 --- a/integration_tests/tests/adapter_query_runner.py +++ b/integration_tests/tests/adapter_query_runner.py @@ -115,8 +115,49 @@ def _create_adapter(project_dir: str, target: str) -> BaseAdapter: # Ref resolution # ------------------------------------------------------------------ + def _build_relation_name(self, node: Dict[str, Any]) -> Optional[str]: + """Construct a relation name from node metadata. + + Uses the adapter's ``Relation`` class so that quoting and include + policies are applied correctly for the target warehouse. + Falls back to simple dot-joining if the adapter rejects the inputs. + """ + schema = node.get("schema") + identifier = node.get("alias") or node.get("name") + if not identifier: + return None + + database = node.get("database") or "" + try: + relation = self._adapter.Relation.create( + database=database, + schema=schema, + identifier=identifier, + ) + return relation.render() + except Exception: + # Some adapters (e.g. ClickHouse) reject certain database values. + # Retry without database, then fall back to simple dot-join. + try: + relation = self._adapter.Relation.create( + database="", + schema=schema, + identifier=identifier, + ) + return relation.render() + except Exception: + parts = [p for p in (schema, identifier) if p] + return ".".join(parts) if parts else None + def _load_manifest_maps(self) -> None: - """Load ref and source maps from the dbt manifest.""" + """Load ref and source maps from the dbt manifest. + + Scans both ``nodes`` (enabled models) and ``disabled`` (models + disabled via config, e.g. ``elementary_enabled = False``). + For disabled nodes whose ``relation_name`` is ``None`` (dbt skips + compilation for disabled nodes), the relation name is synthesised + from ``database`` / ``schema`` / ``alias`` via the adapter. + """ manifest_path = Path(self._project_dir) / "target" / "manifest.json" if not manifest_path.exists(): raise FileNotFoundError( @@ -126,12 +167,28 @@ def _load_manifest_maps(self) -> None: with open(manifest_path) as fh: manifest = json.load(fh) + # Collect every node dict: enabled nodes first, then disabled. + all_nodes: List[Dict[str, Any]] = list(manifest.get("nodes", {}).values()) + disabled = manifest.get("disabled", {}) + if isinstance(disabled, dict): + for versions in disabled.values(): + if isinstance(versions, list): + all_nodes.extend(versions) + else: + all_nodes.append(versions) + ref_map: Dict[str, str] = {} - for node in manifest.get("nodes", {}).values(): - relation_name = node.get("relation_name") + for node in all_nodes: name = node.get("name") - if relation_name and name: - ref_map[name] = relation_name + if not name: + continue + relation_name = node.get("relation_name") + if not relation_name: + relation_name = self._build_relation_name(node) + if relation_name: + # First writer wins — enabled nodes are processed first, + # so a disabled duplicate won't overwrite an enabled one. + ref_map.setdefault(name, relation_name) source_map: Dict[tuple, str] = {} for source in manifest.get("sources", {}).values(): From 7a5051ebccc90323140cceeffdd2833a6d650c8e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 13:26:04 +0000 Subject: [PATCH 2/3] address review: add debug logging to fallback paths, simplify disabled node handling Co-Authored-By: Itamar Hartstein --- integration_tests/tests/adapter_query_runner.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/integration_tests/tests/adapter_query_runner.py b/integration_tests/tests/adapter_query_runner.py index 89f659cbf..97658c9c1 100644 --- a/integration_tests/tests/adapter_query_runner.py +++ b/integration_tests/tests/adapter_query_runner.py @@ -138,6 +138,11 @@ def _build_relation_name(self, node: Dict[str, Any]) -> Optional[str]: except Exception: # Some adapters (e.g. ClickHouse) reject certain database values. # Retry without database, then fall back to simple dot-join. + logger.debug( + "Relation.create failed with database=%r for %s; retrying without database", + database, + identifier, + ) try: relation = self._adapter.Relation.create( database="", @@ -146,6 +151,10 @@ def _build_relation_name(self, node: Dict[str, Any]) -> Optional[str]: ) return relation.render() except Exception: + logger.debug( + "Relation.create failed without database for %s; using dot-join fallback", + identifier, + ) parts = [p for p in (schema, identifier) if p] return ".".join(parts) if parts else None @@ -170,12 +179,8 @@ def _load_manifest_maps(self) -> None: # Collect every node dict: enabled nodes first, then disabled. all_nodes: List[Dict[str, Any]] = list(manifest.get("nodes", {}).values()) disabled = manifest.get("disabled", {}) - if isinstance(disabled, dict): - for versions in disabled.values(): - if isinstance(versions, list): - all_nodes.extend(versions) - else: - all_nodes.append(versions) + for versions in disabled.values(): + all_nodes.extend(versions) ref_map: Dict[str, str] = {} for node in all_nodes: From d6f6b44d91d76aa09cd121edf32c507b8f61eb93 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:05:05 +0000 Subject: [PATCH 3/3] address review: use adapter include_policy instead of try/except for database handling Co-Authored-By: Itamar Hartstein --- .../tests/adapter_query_runner.py | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/integration_tests/tests/adapter_query_runner.py b/integration_tests/tests/adapter_query_runner.py index 97658c9c1..7e924bda6 100644 --- a/integration_tests/tests/adapter_query_runner.py +++ b/integration_tests/tests/adapter_query_runner.py @@ -120,43 +120,23 @@ def _build_relation_name(self, node: Dict[str, Any]) -> Optional[str]: Uses the adapter's ``Relation`` class so that quoting and include policies are applied correctly for the target warehouse. - Falls back to simple dot-joining if the adapter rejects the inputs. + The adapter's ``include_policy`` determines whether ``database`` is + passed (e.g. ClickHouse sets ``database=False``). """ schema = node.get("schema") identifier = node.get("alias") or node.get("name") if not identifier: return None - database = node.get("database") or "" - try: - relation = self._adapter.Relation.create( - database=database, - schema=schema, - identifier=identifier, - ) - return relation.render() - except Exception: - # Some adapters (e.g. ClickHouse) reject certain database values. - # Retry without database, then fall back to simple dot-join. - logger.debug( - "Relation.create failed with database=%r for %s; retrying without database", - database, - identifier, - ) - try: - relation = self._adapter.Relation.create( - database="", - schema=schema, - identifier=identifier, - ) - return relation.render() - except Exception: - logger.debug( - "Relation.create failed without database for %s; using dot-join fallback", - identifier, - ) - parts = [p for p in (schema, identifier) if p] - return ".".join(parts) if parts else None + include_policy = self._adapter.Relation.get_default_include_policy() + database = (node.get("database") or "") if include_policy.database else "" + + relation = self._adapter.Relation.create( + database=database, + schema=schema, + identifier=identifier, + ) + return relation.render() def _load_manifest_maps(self) -> None: """Load ref and source maps from the dbt manifest.