fix(parser): don't reassign a disabled package resource's schema.yml patch to an unrelated same-named node - #16198
Conversation
…patch to an unrelated same-named node
`NodePatchParser.parse_patch()` resolved a models/seeds/snapshots schema.yml
patch with:
unique_id = ref_lookup.get_unique_id(patch.name, self.project.project_name, None) \
or ref_lookup.get_unique_id(patch.name, None, None)
When a package's own resource with `patch.name` is disabled (e.g. via
`+enabled: false` in a dependent project's dbt_project.yml), the scoped
lookup correctly returns None. But the unscoped fallback then searches
*every* package for an enabled node with that name -- so if the root
project (or another package) happens to define its own resource under the
same name, the disabled package's schema.yml patch gets silently bound to
that unrelated node instead of falling through to the "handle disabled
nodes" branch. If that other node already has a patch_path (its own
schema.yml already patched it), this raises DuplicatePatchPathError:
"dbt found two schema.yml entries for the same resource named X" -- even
though only one schema.yml entry actually describes an enabled resource.
This is a common pattern for projects built on a shared package: disable
the package's seed and define a same-named seed in the root project to
override it. Both the package and the root project describe the seed in
their own schema.yml.
Fix: before falling back to the unscoped cross-package lookup, check
whether the resource's own package has a disabled node with this name. If
so, let this patch flow into the existing disabled-node handling instead
of resolving it against an unrelated package's node.
Added a regression test (tests/functional/dependencies/
test_disabled_dependency_seed_schema_patch.py) using a local package
dependency, confirmed it reproduces the exact reported error before the
fix and passes after.
Fixes dbt-labs#15562
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kAvbZ6SeKgp6jbGcvpXSh
There was a problem hiding this comment.
Gates Failed
Prevent hotspot decline
(10 hotspots with Complex Method, Complex Conditional, Bumpy Road Ahead, Overall Code Complexity, Lines of Code in a Single File, Number of Functions in a Single Module, Low Cohesion, Code Duplication, Deep, Nested Complexity, Excess Number of Function Arguments, Large Method)
New code is healthy
(32 new files with code health below 9.00)
Enforce critical code health rules
(30 files with Low Cohesion, Bumpy Road Ahead, Deep, Nested Complexity)
Enforce advisory code health rules
(56 files with Complex Method, Large Method, Excess Number of Function Arguments, Complex Conditional, Overall Code Complexity, Lines of Code in a Single File, Number of Functions in a Single Module, Code Duplication)
Our agent can fix these. Install it.
Reason for failure
| Prevent hotspot decline | Violations | Code Health Impact | |
|---|---|---|---|
| manifest.py | 10 rules in this hotspot | 1.91 | Suppress |
| providers.py | 8 rules in this hotspot | 3.61 | Suppress |
| nodes.py | 7 rules in this hotspot | 3.66 | Suppress |
| schemas.py | 6 rules in this hotspot | 4.12 | Suppress |
| test_manifest.py | 6 rules in this hotspot | 4.93 | Suppress |
| manifest.py | 7 rules in this hotspot | 5.23 | Suppress |
| test_parser.py | 7 rules in this hotspot | 5.99 | Suppress |
| expected_manifest.py | 2 rules in this hotspot | 6.30 | Suppress |
| test_deprecations.py | 4 rules in this hotspot | 7.11 | Suppress |
| project.py | 4 rules in this hotspot | 8.47 | Suppress |
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| manifest.py | 10 rules | 1.91 | Suppress |
| partial.py | 7 rules | 2.04 | Suppress |
| providers.py | 8 rules | 3.61 | Suppress |
| nodes.py | 7 rules | 3.66 | Suppress |
| schemas.py | 6 rules | 4.12 | Suppress |
| test_manifest.py | 6 rules | 4.93 | Suppress |
| manifest.py | 7 rules | 5.23 | Suppress |
| runnable.py | 6 rules | 5.72 | Suppress |
| test_list.py | 4 rules | 5.86 | Suppress |
| unit_tests.py | 6 rules | 5.96 | Suppress |
| test_parser.py | 7 rules | 5.99 | Suppress |
| jsonschemas.py | 5 rules | 6.07 | Suppress |
| run.py | 6 rules | 6.11 | Suppress |
| expected_manifest.py | 2 rules | 6.30 | Suppress |
| read_files.py | 6 rules | 6.52 | Suppress |
| flags.py | 4 rules | 6.62 | Suppress |
| test_deprecations.py | 4 rules | 7.11 | Suppress |
| jinja_static.py | 5 rules | 7.29 | Suppress |
| base.py | 3 rules | 7.33 | Suppress |
| test_project.py | 2 rules | 7.46 | Suppress |
| fusion.py | 4 rules | 7.71 | Suppress |
| project.py | 5 rules | 7.77 | Suppress |
| list.py | 4 rules | 7.87 | Suppress |
| test_run.py | 3 rules | 8.03 | Suppress |
| tracking.py | 1 rule | 8.28 | Suppress |
| main.py | 1 rule | 8.28 | Suppress |
| project.py | 4 rules | 8.47 | Suppress |
| requires.py | 3 rules | 8.52 | Suppress |
| test_fusion.py | 2 rules | 8.55 | Suppress |
| option_types.py | 4 rules | 8.68 | Suppress |
| artifact_upload.py | 2 rules | 8.80 | Suppress |
| runtime.py | 4 rules | 8.82 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| manifest.py | 3 critical rules | 1.91 | Suppress |
| partial.py | 2 critical rules | 2.04 | Suppress |
| providers.py | 2 critical rules | 3.61 | Suppress |
| nodes.py | 3 critical rules | 3.66 | Suppress |
| schemas.py | 2 critical rules | 4.12 | Suppress |
| test_manifest.py | 2 critical rules | 4.93 | Suppress |
| manifest.py | 2 critical rules | 5.23 | Suppress |
| runnable.py | 2 critical rules | 5.72 | Suppress |
| test_list.py | 2 critical rules | 5.86 | Suppress |
| unit_tests.py | 2 critical rules | 5.96 | Suppress |
| test_parser.py | 1 critical rule | 5.99 | Suppress |
| jsonschemas.py | 2 critical rules | 6.07 | Suppress |
| run.py | 2 critical rules | 6.11 | Suppress |
| read_files.py | 2 critical rules | 6.52 | Suppress |
| flags.py | 1 critical rule | 6.62 | Suppress |
| test_deprecations.py | 1 critical rule | 7.11 | Suppress |
| jinja_static.py | 2 critical rules | 7.29 | Suppress |
| base.py | 2 critical rules | 7.33 | Suppress |
| test_project.py | 1 critical rule | 7.46 | Suppress |
| fusion.py | 1 critical rule | 7.71 | Suppress |
| project.py | 2 critical rules | 7.77 | Suppress |
| list.py | 2 critical rules | 7.87 | Suppress |
| test_run.py | 1 critical rule | 8.03 | Suppress |
| project.py | 1 critical rule | 8.47 | Suppress |
| requires.py | 1 critical rule | 8.52 | Suppress |
| test_fusion.py | 1 critical rule | 8.55 | Suppress |
| option_types.py | 1 critical rule | 8.68 | Suppress |
| runtime.py | 1 critical rule | 8.82 | Suppress |
| macros.py | 1 critical rule | 9.10 | Suppress |
| init.py | 1 critical rule | 9.28 | Suppress |
| Enforce advisory code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| manifest.py | 7 advisory rules | 1.91 | Suppress |
| partial.py | 5 advisory rules | 2.04 | Suppress |
| providers.py | 6 advisory rules | 3.61 | Suppress |
| nodes.py | 4 advisory rules | 3.66 | Suppress |
| schemas.py | 4 advisory rules | 4.12 | Suppress |
| test_manifest.py | 4 advisory rules | 4.93 | Suppress |
| manifest.py | 5 advisory rules | 5.23 | Suppress |
| runnable.py | 4 advisory rules | 5.72 | Suppress |
| test_list.py | 2 advisory rules | 5.86 | Suppress |
| unit_tests.py | 4 advisory rules | 5.96 | Suppress |
| test_parser.py | 6 advisory rules | 5.99 | Suppress |
| jsonschemas.py | 3 advisory rules | 6.07 | Suppress |
| run.py | 4 advisory rules | 6.11 | Suppress |
| expected_manifest.py | 2 advisory rules | 6.30 | Suppress |
| read_files.py | 4 advisory rules | 6.52 | Suppress |
| flags.py | 3 advisory rules | 6.62 | Suppress |
| test_deprecations.py | 3 advisory rules | 7.11 | Suppress |
| jinja_static.py | 3 advisory rules | 7.29 | Suppress |
| base.py | 1 advisory rule | 7.33 | Suppress |
| test_project.py | 1 advisory rule | 7.46 | Suppress |
| fusion.py | 3 advisory rules | 7.71 | Suppress |
| project.py | 3 advisory rules | 7.77 | Suppress |
| list.py | 2 advisory rules | 7.87 | Suppress |
| test_run.py | 2 advisory rules | 8.03 | Suppress |
| tracking.py | 1 advisory rule | 8.28 | Suppress |
| main.py | 1 advisory rule | 8.28 | Suppress |
| project.py | 3 advisory rules | 8.47 | Suppress |
| requires.py | 2 advisory rules | 8.52 | Suppress |
| test_fusion.py | 1 advisory rule | 8.55 | Suppress |
| option_types.py | 3 advisory rules | 8.68 | Suppress |
| artifact_upload.py | 2 advisory rules | 8.80 | Suppress |
| runtime.py | 3 advisory rules | 8.82 | Suppress |
| test_configs_in_schema_files.py | 2 advisory rules | 9.02 | Suppress |
| macros.py | 2 advisory rules | 9.10 | Suppress |
| test_context.py | 2 advisory rules | 9.10 | Suppress |
| test_freshness.py | 2 advisory rules | 9.10 | Suppress |
| test_requires.py | 2 advisory rules | 9.24 | Suppress |
| init.py | 1 advisory rule | 9.28 | Suppress |
| test_partial.py | 1 advisory rule | 9.33 | Suppress |
| osi.py | 1 advisory rule | 9.39 | Suppress |
| test_list.py | 1 advisory rule | 9.39 | Suppress |
| test_macro_annotations.py | 1 advisory rule | 9.39 | Suppress |
| test_metric_configs.py | 1 advisory rule | 9.39 | Suppress |
| build.py | 1 advisory rule | 9.39 | Suppress |
| test_jsonschemas.py | 1 advisory rule | 9.39 | Suppress |
| freshness.py | 2 advisory rules | 9.39 | Suppress |
| test_jinja.py | 1 advisory rule | 9.39 | Suppress |
| test_deprecated_version.py | 1 advisory rule | 9.39 | Suppress |
| test_dbt_runner.py | 1 advisory rule | 9.55 | Suppress |
| test_manifest.py | 1 advisory rule | 9.69 | Suppress |
| test_flags.py | 1 advisory rule | 9.69 | Suppress |
| test_tracking.py | 1 advisory rule | 9.69 | Suppress |
| test_warn_error_options.py | 1 advisory rule | 9.69 | Suppress |
| configured.py | 1 advisory rule | 9.69 | Suppress |
| macros.py | 1 advisory rule | 9.69 | Suppress |
| jinja.py | 1 advisory rule | 9.69 | Suppress |
Quality Gate Profile: Clean Code Collective
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is large and cross-cutting (parser/partial parsing/CLI flags/telemetry/artifacts/workflows), so it needs careful human review to validate interactions and backward-compatibility.
Pull request overview
This PR addresses a parsing bug where a schema.yml patch belonging to a disabled resource in its own package could be incorrectly rebound (via an unscoped fallback lookup) to an unrelated same-named enabled resource in another package, causing spurious DuplicatePatchPathError (Fixes #15562). The change also includes several additional updates spanning parsing, partial parsing, telemetry, OpenTelemetry instrumentation, OSI discovery, and test coverage.
Changes:
- Fix
NodePatchParser.parse_patch()to avoid unscoped cross-package patch lookup when a same-named disabled node exists in the patch’s own package; add regression coverage. - Introduce/expand user-facing “hints” + related telemetry hooks; add OpenTelemetry context identifiers in Jinja context and additional OTEL span instrumentation.
- Improve partial parsing cleanup and manifest/data structure behavior (including unit-test post-parse model resolution, OSI path handling, and lazy flat_graph/WritableManifest views).
File summaries
| File | Description |
|---|---|
| tests/unit/utils/project.py | Add osi_paths to the test Project fixture. |
| tests/unit/test_tracking.py | Add unit tests for hint-view + manage-state tracking events. |
| tests/unit/test_jsonschemas.py | Add project-schema validation coverage for Databricks config keys. |
| tests/unit/test_hints.py | New unit tests for hint cooldown/persistence and show/telemetry behavior. |
| tests/unit/task/test_list.py | Add tests ensuring direct_parents is emitted correctly in dbt ls --output=json. |
| tests/unit/task/test_freshness.py | Assert metadata freshness fallback returns RunStatus.Success. |
| tests/unit/task/test_build.py | Add tests for reuse-relations hint gating and unit-test node-count bookkeeping. |
| tests/unit/parser/test_unit_tests.py | Update unit-test parsing tests for deferred (post-parse) model resolution + new regressions. |
| tests/unit/parser/test_partial.py | Add regressions for partial-parse cleanup of disabled tests. |
| tests/unit/parser/test_parser.py | Update macro parsing expectations to include macro arguments. |
| tests/unit/parser/test_manifest.py | Add tests for direct_parents backfill and long-parse hint logic. |
| tests/unit/contracts/graph/test_manifest.py | Add msgpack round-trip test for manifest with lazy flat_graph views. |
| tests/unit/context/test_context.py | Add regression for package/macro name collisions in macro namespace resolution. |
| tests/unit/context/test_base.py | Add tests for new otel_trace_id() / otel_span_id() context members. |
| tests/unit/config/test_project.py | Update Project.__str__ expectations for osi-paths. |
| tests/unit/config/test_fusion_warn_error_options.py | New unit tests for tolerating Fusion-only warn/error option names. |
| tests/unit/clients/test_jinja.py | Add tests for hook-count detection and hook-span naming helpers. |
| tests/unit/clients/test_jinja_static.py | Expand unrendered-config static parsing test matrix + “no config() call” cases. |
| tests/unit/cli/test_flags.py | Add tests for manage-state source attribution + snowflake-projects-otel flag. |
| tests/unit/artifacts/test_run_execution_result.py | Add tests for state_decision_id serialization behavior. |
| tests/functional/unit_testing/test_ut_versioned_parse_order.py | New functional regression for unit-test YAML parse order independence. |
| tests/functional/semantic_models/test_osi_parsing.py | Switch OSI directory handling to lowercase osi/ and adjust assertions. |
| tests/functional/partial_parsing/test_pp_unit_tests.py | New functional tests for partial-parse behavior around unit tests + model changes/disable/delete. |
| tests/functional/partial_parsing/test_pp_semantic_models.py | New functional regression for inline metric cleanup across partial parses. |
| tests/functional/partial_parsing/test_pp_data_tests.py | New functional regression for disabled/enabled toggling without ghost accumulation. |
| tests/functional/metrics/test_metrics.py | Add time spine YAML fixture; adjust expected metric set. |
| tests/functional/metrics/test_metric_configs.py | Add time spine YAML fixture across metric-config scenarios. |
| tests/functional/metrics/fixtures.py | Add time spine YAML; adjust metric fixtures to nested cumulative params. |
| tests/functional/macros/test_macros.py | Add regression for project var values containing Jinja. |
| tests/functional/macros/test_macro_annotations.py | Disable macro-arg validation for a non-enforcement test via project flags. |
| tests/functional/list/test_list.py | Update functional dbt ls JSON expectations to include direct_parents. |
| tests/functional/hints/test_hints.py | New functional tests for hints, cooldown behavior, and flag-based suppression. |
| tests/functional/functions/test_overloaded_udfs.py | Add regression ensuring overload-body dependencies are carried onto root function node. |
| tests/functional/deprecations/test_deprecations.py | Update patch points for renamed adapter-config “extra keys” helpers. |
| tests/functional/dependencies/test_disabled_dependency_seed_schema_patch.py | New functional regression for #15562 disabled dependency seed patch behavior. |
| tests/functional/defer_state/test_modified_state_schema_evolution.py | Add scenario enabling “compare more unrendered values” flag. |
| tests/functional/configs/test_warn_error_options.py | Add functional coverage for ignoring Fusion-only warn/error option names. |
| tests/functional/configs/test_configs_in_schema_files.py | Explicitly set state_modified_compare_more_unrendered_values: false in test project config. |
| tests/functional/cli/test_v2_announcement.py | New functional tests for v2 announcement firing on --version. |
| tests/functional/cli/test_requires.py | Add allow-listed env vars for hints + snowflake OTEL. |
| tests/functional/cli/test_deprecated_version.py | New functional tests for deprecated-version messaging/levels across entry points. |
| tests/functional/artifacts/expected_manifest.py | Update expected unrendered_config contents for snapshots. |
| tests/conftest.py | Add session-scoped OTEL tracer provider + per-test span isolation fixture. |
| docker/Dockerfile | Update pinned OS package versions. |
| core/pyproject.toml | Bump dependency bounds; add opentelemetry-api. |
| core/hatch.toml | Add opentelemetry-sdk to dev/test dependencies. |
| core/dbt/v2_announcement.py | New module emitting v2 availability message. |
| core/dbt/utils/artifact_upload.py | Add Azure upload headers + safer error-code formatting to avoid leaking signed-request data. |
| core/dbt/tracking.py | Add hint-view/manage-state/deprecated-version tracking helpers + shared context-event helper. |
| core/dbt/task/list.py | Re-inject direct_parents into dbt ls --output=json output. |
| core/dbt/task/init.py | Add deprecated-version warning on dbt init. |
| core/dbt/task/freshness.py | Treat metadata freshness batch failure as non-fatal fallback (RunStatus.Success). |
| core/dbt/task/build.py | Add reuse-relations hint logic; fix unit-test node-count accounting; propagate OTEL context into unit-test runner calls. |
| core/dbt/parser/unit_tests.py | Make unit-test model resolution deterministic by deferring it to a post-parse pass; improve disabled routing. |
| core/dbt/parser/schemas.py | Fix disabled-node patch reassignment bug (core fix for #15562); merge overload dependencies onto root functions; default macro arg validation to on. |
| core/dbt/parser/read_files.py | Read OSI files from configurable osi_paths instead of a single constant directory. |
| core/dbt/parser/partial.py | Clean up disabled tests and inline metrics correctly during partial parsing to prevent ghost duplicates. |
| core/dbt/parser/osi.py | Support multiple OSI directories with directory deduplication; clear OSI-attributed nodes using multiple prefixes. |
| core/dbt/parser/macros.py | Default macro-arg validation to on when flag is absent. |
| core/dbt/jsonschemas/jsonschemas.py | Rename “config aliases” → “extra keys”; add Databricks adapter config keys as allowed extras. |
| core/dbt/hints.py | New hints implementation with cooldown persistence + hint telemetry. |
| core/dbt/flags.py | Include hints_enabled in flag dict export. |
| core/dbt/deprecated_version.py | New deprecated-version notice + telemetry. |
| core/dbt/contracts/project.py | Add osi_paths; add project flags for hints + snowflake OTEL; flip several legacy flag defaults. |
| core/dbt/contracts/graph/nodes.py | Add direct_parents to ModelNode but strip it from manifest serialization. |
| core/dbt/contracts/graph/node_args.py | Add direct_parents to ModelNodeArgs. |
| core/dbt/contracts/graph/manifest.py | Make flat_graph + WritableManifest mappings lazy/no-copy and rebuild flat_graph after deserialization. |
| core/dbt/context/providers.py | Pass BaseContext into Var helpers (typed) rather than a raw dict. |
| core/dbt/context/manifest.py | Use a ChainMap-based optimization to avoid eagerly copying large macro namespaces into the context dict. |
| core/dbt/context/configured.py | Pass BaseContext into ConfiguredVar helpers (typed) rather than a raw dict. |
| core/dbt/context/base.py | Add otel_trace_id() and otel_span_id() context members; make Var rendering read from the current context mapping. |
| core/dbt/constants.py | Change OSI directory constant to lowercase osi. |
| core/dbt/config/utils.py | Add Fusion-only warn/error option partitioning + tolerant WarnErrorOptionsV2 construction. |
| core/dbt/config/runtime.py | Propagate osi_paths into RuntimeConfig. |
| core/dbt/config/project.py | Default osi_paths to both osi and OSI; serialize osi-paths in project config output. |
| core/dbt/clients/jinja.py | Add OTEL hook-span instrumentation for run_hooks and safer hook-phase identification helpers. |
| core/dbt/clients/jinja_static.py | Improve reconstruction of config() kwarg values to be more source-like and adapter-independent. |
| core/dbt/cli/requires.py | Add manage-state telemetry emission and deprecated-version checks; plumb snowflake OTEL enablement into invocation context. |
| core/dbt/cli/params.py | Add --hints-enabled and --snowflake-projects-otel; extend --version behavior to announce v2 + deprecated-version warning. |
| core/dbt/cli/option_types.py | Route warn-error-options parsing through Fusion-tolerant builder. |
| core/dbt/cli/main.py | Register new global flags. |
| core/dbt/cli/flags.py | Build warn-error-options via Fusion-tolerant builder; add MANAGE_STATE_SOURCE attribution. |
| core/dbt/cli/exceptions.py | Add mypy ignore for Click exit-code typing. |
| core/dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py | Upgrade stored manifest unrendered_config entries when “compare more unrendered values” is enabled. |
| core/dbt/artifacts/schemas/run/v5/run.py | Add state_decision_id to RunResult and its message dict representation. |
| core/dbt/artifacts/resources/v1/catalog.py | Add catalog_database field to catalog write integration config. |
| core/dbt/version.py | Bump version to 1.14.0a1. |
| CHANGELOG.md | Add links to 1.12 and 1.13 changelogs. |
| .github/workflows/release.yml | Fix hatch version comparison logic (integer compare vs decimal). |
| .github/workflows/docs-issue.yml | Update docs label from user docs to action:needs-docs. |
| .github/workflows/community-pr-notify.yml | New Slack notification workflow for PRs labeled community. |
| .github/workflows/backport.yml | Trigger backport workflow on merged+closed labeled PRs (and merged already-labeled PRs). |
| .github/pull_request_template.md | Update label guidance to action:needs-docs. |
| .github/ISSUE_TEMPLATE/implementation-ticket.yml | Update default label to action:needs-docs. |
| .changes/unreleased/* | Add/update changie entries reflecting the above changes (including #15562 fix). |
| .changes/0.0.0.md | Add links to 1.11–1.13 changelogs. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Flagging why the bot checks look bad here — none of it reflects this diff. core/dbt/parser/schemas.py doesn't exist on main; main is the Rust v2.0 The CodeScene job is "CodeScene Code Health Review (main)" — it analyses Unrelated, but both CONTRIBUTING.md files still say community PRs should |
NodePatchParser.parse_patch() resolves a models/seeds/snapshots schema.yml patch by first looking it up scoped to its own package, then falling back to an unscoped, cross-package lookup if that fails. When a package's own resource is disabled (e.g. via +enabled: false in a dependent project), the scoped lookup correctly returns nothing — but the unscoped fallback can then find and bind the patch to an unrelated same-named resource in a different package. If that other resource already has its own patch, this raises DuplicatePatchPathError even though only one schema.yml entry actually describes an enabled resource.
This is a common pattern: disable a shared package's seed and define a same-named seed in the root project to override it. Both describe the seed in their own schema.yml.
Fix: before falling back to the unscoped lookup, check whether the resource's own package has a disabled node with this name — if so, let the patch flow into the existing disabled-node handling instead.
Added a regression test using a local package dependency (tests/functional/dependencies/test_disabled_dependency_seed_schema_patch.py) — confirmed it reproduces the exact reported error before the fix and passes after, run against a real Postgres instance.
Fixes #15562