Skip to content

[dagster-dbt] Honor DBT_PROFILE/DBT_TARGET in the in-process adapter#33937

Open
Vamsi-klu wants to merge 1 commit into
dagster-io:masterfrom
Vamsi-klu:dbt-honor-target-profile-env-vars
Open

[dagster-dbt] Honor DBT_PROFILE/DBT_TARGET in the in-process adapter#33937
Vamsi-klu wants to merge 1 commit into
dagster-io:masterfrom
Vamsi-klu:dbt-honor-target-profile-env-vars

Conversation

@Vamsi-klu

Copy link
Copy Markdown

Summary & Motivation

Fixes #33818.

DbtCliResource resolves the dbt profile/target in two different places:

  • Subprocess path (dbt run, dbt build, …): the subprocess inherits os.environ, so dbt's own CLI layer reads DBT_PROFILE / DBT_TARGET (see dbt/cli/params.py) and selects that profile/target.
  • In-process adapter path (_initialize_dbt_core_adapter, used for metadata calls such as column metadata): calls dbt's load_profile(self.project_dir, cli_vars, self.profile, self.target). self.profile/self.target are None unless explicitly set on the resource, and dbt's load_profile does not read the env vars itself — only its Click CLI layer does.

As a result, with DBT_TARGET=prod set in the environment, the subprocess uses prod while the in-process adapter falls back to the profiles.yml default (e.g. dev), so metadata calls connect to the wrong warehouse/credentials. This is the exact symptom reported in #33818.

The fix mirrors dbt's CLI resolution: fall back to DBT_PROFILE / DBT_TARGET when the resource does not set profile/target explicitly. Explicit resource configuration still takes precedence, matching dbt's documented --profile/--target override semantics.

profile_override = self.profile or os.getenv("DBT_PROFILE")
target_override = self.target or os.getenv("DBT_TARGET")
profile = load_profile(self.project_dir, cli_vars, profile_override, target_override)

(DBT_PROFILES_DIR is a separate, lower-priority gap mentioned in the issue title; the reproduced symptom is DBT_TARGET/DBT_PROFILE, so this PR is kept minimal to those.)

How I Tested These Changes

Added a parametrized unit test in dagster_dbt_tests/core/test_resource.py that patches dbt's load_profile and asserts the exact profile/target overrides Dagster passes to it, then short-circuits before any real adapter/warehouse connection:

  • DBT_PROFILE/DBT_TARGET are honored when the resource sets neither;
  • explicit profile=/target= on the resource win over the env vars;
  • with neither set, None is passed through so dbt applies its own default resolution.

ruff check and ruff format --check pass on both changed files.

Disclosure: I could not execute pytest locally because dbt fails to import in my environment (mashumaro UnserializableField on Python 3.14). The test is designed to need no working warehouse/dbt CLI — only an importable dbt — so it is expected to run in CI on the supported Python matrix. Please flag if CI surfaces anything.

Changelog

[dagster-dbt] DbtCliResource now honors the DBT_PROFILE and DBT_TARGET environment variables in its in-process adapter (used for metadata), matching how the dbt subprocess already resolves them. Explicit profile/target configuration still takes precedence.


Prepared with AI assistance (Claude Code).

The dbt subprocess spawned by DbtCliResource inherits os.environ, so dbt's
CLI layer reads DBT_PROFILE / DBT_TARGET and selects that profile/target.
The in-process adapter used for metadata calls (e.g. column metadata),
however, called load_profile with self.profile / self.target only, which
are None unless set explicitly on the resource. As a result the two paths
could resolve different profiles/targets (e.g. subprocess -> prod, metadata
adapter -> the profiles.yml default), causing wrong-credential errors.

Fall back to the DBT_PROFILE / DBT_TARGET environment variables when the
resource does not set them explicitly, matching dbt's own CLI resolution.
Explicit resource configuration still takes precedence.

Fixes dagster-io#33818
@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a parity gap in DbtCliResource: the dbt subprocess inherits os.environ and therefore already honors DBT_PROFILE/DBT_TARGET, but the in-process adapter path (used for metadata such as column metadata) called load_profile with self.profile/self.target directly — bypassing those env vars — so both paths could resolve different profiles.

  • resource.py: Two lines added before load_profile read DBT_PROFILE/DBT_TARGET as fallbacks when the resource fields are not explicitly set, making in-process and subprocess profile resolution consistent.
  • test_resource.py: A parametrized unit test patches dbt.config.runtime.load_profile and asserts the correct override values are passed under three scenarios (env-only, explicit-arg-wins, neither set).

Confidence Score: 5/5

Safe to merge — the change is two lines of fallback reads from the environment, explicit resource config continues to take precedence, and the fix is scoped to the single in-process code path that was inconsistent.

The production change is minimal and correct: it reads two well-known dbt env vars as fallbacks when the resource fields are unset, mirroring what the subprocess path already receives for free. Explicit configuration still wins. No state is mutated, no new dependencies are introduced, and os.getenv returns None (matching the prior default) when the vars are absent.

No files require special attention. The test depends on dbt's internal set_invocation_context/set_from_args/get_flags succeeding without mocks, but that is a pre-existing pattern in the test suite and not a correctness risk for the production change.

Important Files Changed

Filename Overview
python_modules/libraries/dagster-dbt/dagster_dbt/core/resource.py Two-line fix: reads DBT_PROFILE/DBT_TARGET from the environment as fallbacks when the resource fields are None, then passes them to load_profile. Logic is correct for the common case; os was already imported at module level.
python_modules/libraries/dagster-dbt/dagster_dbt_tests/core/test_resource.py Adds a parametrized test covering env-var honored, explicit-arg-wins, and no-override scenarios. Patches dbt.config.runtime.load_profile correctly (local import inside function body makes the module-level patch work). Test depends on pre-load_profile dbt calls (set_invocation_context, set_from_args, get_flags) succeeding without mocks.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User as User Code
    participant Res as DbtCliResource
    participant Env as os.environ
    participant LP as dbt load_profile

    Note over User,LP: Subprocess path (pre-existing)
    User->>Res: .cli(["dbt", "run"])
    Res->>Env: subprocess inherits os.environ
    Env-->>LP: DBT_PROFILE / DBT_TARGET via Click CLI layer

    Note over User,LP: In-process adapter path (this PR)
    User->>Res: _initialize_dbt_core_adapter(args)
    Res->>Res: "profile_override = self.profile or os.getenv("DBT_PROFILE")"
    Res->>Res: "target_override = self.target or os.getenv("DBT_TARGET")"
    Res->>LP: load_profile(project_dir, cli_vars, profile_override, target_override)
    LP-->>Res: profile object (same warehouse as subprocess)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User as User Code
    participant Res as DbtCliResource
    participant Env as os.environ
    participant LP as dbt load_profile

    Note over User,LP: Subprocess path (pre-existing)
    User->>Res: .cli(["dbt", "run"])
    Res->>Env: subprocess inherits os.environ
    Env-->>LP: DBT_PROFILE / DBT_TARGET via Click CLI layer

    Note over User,LP: In-process adapter path (this PR)
    User->>Res: _initialize_dbt_core_adapter(args)
    Res->>Res: "profile_override = self.profile or os.getenv("DBT_PROFILE")"
    Res->>Res: "target_override = self.target or os.getenv("DBT_TARGET")"
    Res->>LP: load_profile(project_dir, cli_vars, profile_override, target_override)
    LP-->>Res: profile object (same warehouse as subprocess)
Loading

Reviews (2): Last reviewed commit: "[dagster-dbt] Honor DBT_PROFILE/DBT_TARG..." | Re-trigger Greptile

Comment on lines +396 to +397
profile_override = self.profile or os.getenv("DBT_PROFILE")
target_override = self.target or os.getenv("DBT_TARGET")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Using or for the fallback means an explicitly-set empty string (profile="") would be treated as falsy and the env-var would win instead of the explicit value. While pydantic doesn't enforce non-empty strings on these fields, a more defensive check makes the intent unambiguous and matches how other dbt tooling handles Optional[str] overrides.

Suggested change
profile_override = self.profile or os.getenv("DBT_PROFILE")
target_override = self.target or os.getenv("DBT_TARGET")
profile_override = self.profile if self.profile is not None else os.getenv("DBT_PROFILE")
target_override = self.target if self.target is not None else os.getenv("DBT_TARGET")

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +299 to +306
class _StopAdapterInit(Exception):
"""Raised to short-circuit adapter init right after profile resolution."""

monkeypatch.delenv("DBT_PROFILE", raising=False)
monkeypatch.delenv("DBT_TARGET", raising=False)
for key, value in env.items():
monkeypatch.setenv(key, value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test may not reach the mocked load_profile

Several real dbt calls run inside _initialize_dbt_core_adapter before load_profile is invoked — specifically set_invocation_context, set_from_args, and get_flags. These are not mocked, so if dbt's internal state isn't clean between parametrize runs (e.g. flags or adapter registry left from a previous test), one of those earlier calls could raise an unexpected exception. pytest.raises(_StopAdapterInit) would not catch it, causing a confusing failure that looks unrelated to the env-var resolution being tested. Mocking or resetting dbt's flags/adapter state around the test body would make the isolation tighter.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0c60a81f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +396 to +397
profile_override = self.profile or os.getenv("DBT_PROFILE")
target_override = self.target or os.getenv("DBT_TARGET")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate DBT env overrides by dbt version

In dbt-core 1.7.x, which validate_dbt_version still accepts, the dbt CLI options for --profile and --target do not read DBT_PROFILE / DBT_TARGET (those envvars were added in dbt 1.8), so the subprocess will keep using the project/default profile while these lines force the in-process adapter to use the environment override. In a supported 1.7 deployment with DBT_TARGET=prod set, metadata calls can now connect to a different target than the actual dbt invocation; only apply this fallback for dbt versions whose CLI honors the same envvars.

Useful? React with 👍 / 👎.

Comment on lines +396 to +397
profile_override = self.profile or os.getenv("DBT_PROFILE")
target_override = self.target or os.getenv("DBT_TARGET")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect CLI flags before env fallback

When a caller supplies --target or --profile in args while DBT_TARGET / DBT_PROFILE is also set, the subprocess receives those explicit flags via full_dbt_args and dbt/Click gives them precedence over envvars, but the in-process adapter never parses args and instead selects the env override here whenever the resource fields are unset. In that scenario, metadata fetching can connect to the env target/profile while the actual dbt run uses the CLI-specified one; parse the CLI flags before falling back to the environment.

Useful? React with 👍 / 👎.

# env vars when the resource does not set profile/target explicitly so both
# paths resolve the same profile. Explicit resource config still wins.
# Fixes https://github.com/dagster-io/dagster/issues/33818
profile_override = self.profile or os.getenv("DBT_PROFILE")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor DBT_PROFILES_DIR with DBT_PROFILE

When DBT_PROFILE is set together with DBT_PROFILES_DIR and the resource has no explicit profiles_dir, the subprocess inherits DBT_PROFILES_DIR, but _initialize_dbt_core_adapter still sets dbt flags to self.project_dir before calling load_profile. This new fallback can therefore look up the env-selected profile in the wrong profiles directory, either failing adapter initialization or using same-named credentials from the project directory while the actual dbt run uses the env profiles directory.

Useful? React with 👍 / 👎.

@Vamsi-klu

Copy link
Copy Markdown
Author

This is ready for review. It's a one-line fix in resource.py so the in-process dbt adapter honors DBT_PROFILE/DBT_TARGET the same way the subprocess invocation already does, plus a parametrized test (env honored, explicit args win, no-override case). Ruff-clean.

For transparency: I could not run the dbt test suite locally (a dbt/mashumaro import error under Python 3.14 in my environment), so I'm relying on CI for the full run. Happy to make any changes.


Drafted-by: Claude Code (Opus 4.8) (no human review before posting)

@Vamsi-klu Vamsi-klu force-pushed the dbt-honor-target-profile-env-vars branch from b0c60a8 to 8a184cb Compare June 20, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[dagster-dbt] In-process adapter init does not respect DBT_TARGET/DBT_PROFILE env vars for post-processing/metadata fetches

1 participant