Skip to content

feat(context): warn when load_result returns None during parse phase (#11070) - #12815

Closed
SarthakB11 wants to merge 2 commits into
dbt-labs:main_backupfrom
SarthakB11:feat/11070-warn-load-result-parse-phase
Closed

SarthakB11 wants to merge 2 commits into
dbt-labs:main_backupfrom
SarthakB11:feat/11070-warn-load-result-parse-phase

Conversation

@SarthakB11

@SarthakB11 SarthakB11 commented Apr 11, 2026 •

Copy link
Copy Markdown

Fixes #11070.

What

Extends ProviderContext.load_result in core/dbt/context/providers.py so that when it's about to return None for a name that was never stored, and the provider is in parse mode (self.provider.execute is False), it fires a JinjaLogWarning pointing the user at the {% if execute %} docs.

Why

Quoting @joellabes on the issue:

Any Jinja that relies on a result being returned from the database will error during the parse phase. For example [...]

Encountered an error:
Compilation Error in model order_payment_methods (models/order_payment_methods.sql)
'None' has no attribute 'table'

and the alternative suggestion:

we could also add a warning line inside of run_query and friends so you wind up with something like this:

WARN: run_query was invoked in the model order_payment_methods, but dbt is currently not running in execute mode so accessing the result will fail. Wrap in an if execute block https://docs.getdbt.com/reference/dbt-jinja-functions/execute

@graciegoheen on 2026-03-10:

Good call! This would definitely help improve debugging.

(Also noting that #12736, a prior attempt, was self-closed by @claygeo on 2026-03-30 for queue management, not on merits — so the direction is still open.)

Where

The run_query macro path is:

  1. User calls {% set r = run_query(sql) %} in a model.
  2. run_query (defined in dbt-adapters) calls {% call statement("run_query_statement", ...) %} and then load_result("run_query_statement").
  3. During parse, execute is False, statement()'s body is skipped, nothing is stored.
  4. load_result("run_query_statement") drops into the else branch of ProviderContext.load_result in dbt-core and returns None.
  5. User code dereferences None.table / None.columns → the cryptic compile error.

That else branch is a narrow, dbt-core-owned choke point that sees every run_query call during parse, which is why it's a better hook than trying to patch run_query or statement in dbt-adapters.

Diff summary

else:
    # Handle trying to load a result that was never stored.
    #
    # During the parse phase `execute == False`, so no SQL has been run
    # and `sql_results` is always empty. A `load_result` call that
    # lands here is almost always a user who forgot to wrap an
    # introspective query (e.g. `run_query` / `load_result`) in an
    # `{% if execute %}` block — which later surfaces as the cryptic
    # "'None' has no attribute 'table'" compilation error. Emit an
    # upfront warning pointing at the execute docs so the root cause
    # is visible before the downstream error.
    #
    # See dbt-labs/dbt-core#11070.
    if not self.provider.execute:
        model_name = getattr(self.model, "name", None) or getattr(
            self.model, "unique_id", "<unknown>"
        )
        fire_event(
            JinjaLogWarning(
                msg=(
                    f"`load_result('{name}')` was called in {model_name} during "
                    "the parse phase and returned None because no SQL has been "
                    "run yet. Wrap introspective calls like `run_query` / "
                    "`load_result` in an `{% if execute %}` block — see "
                    "https://docs.getdbt.com/reference/dbt-jinja-functions/execute"
                )
            )
        )
    return None

Uses the same fire_event(JinjaLogWarning(msg=...)) pattern already used in store_seed_results a few hundred lines below, so no new imports are needed.

Behaviour

  • Parse phase + name not stored → warns (this is the new behaviour, targeting the reported bug).
  • Execute phase + name not stored → unchanged: returns None silently. A missing name in execute mode is a real user bug (typo, or calling load_result on a name never wrapped in a statement block) and should not be converted into a spurious "wrap in execute" warning.
  • Both phases + name in sql_results → completely unchanged.

Tests

  • No unit test added in this PR — the existing tests/unit/context/test_context.py only validates the set of keys available on the context, and there's no existing load_result behavioral test harness. Adding one would require spinning up a full ProviderContext with model fixtures; happy to add that in a follow-up if a maintainer prefers.
  • Smoke-tested the branch logic with a minimal fake ProviderContext (parse + unknown name → warns; execute + unknown name → no warn; _sql_results-present branches unchanged).
  • python -m py_compile core/dbt/context/providers.py passes.

Changelog

Added .changes/unreleased/Features-20260411-184500.yaml via the standard changie format.

@SarthakB11
SarthakB11 requested a review from a team as a code owner April 11, 2026 19:49
@cla-bot

cla-bot Bot commented Apr 11, 2026

Copy link
Copy Markdown

Thanks for your pull request, and welcome to our community! We require contributors to sign our Contributor License Agreement and we don't seem to have your signature on file. Check out this article for more information on why we have a CLA.

In order for us to review and merge your code, please submit the Individual Contributor License Agreement form attached above above. If you have questions about the CLA, or if you believe you've received this message in error, please reach out through a comment on this PR.

CLA has not been signed by users: @SarthakB11

@github-actions github-actions Bot added the source:community Reported by an external community member (open-source user, not a paying customer). label Apr 11, 2026
…abs#11070)

When a dbt user calls an introspective helper like `run_query` or
`load_result` from a model body without wrapping it in an
`{% if execute %}` block, the result silently comes back as None
during the parse phase. The failure then surfaces as the cryptic

    Compilation Error in model ...
    'None' has no attribute 'table'

which is hard to debug unless you already know about parse vs
execute phases.

This patch extends `ProviderContext.load_result` so that when it is
about to return None for a name that was never stored, and the
provider is in parse mode (`self.provider.execute is False`), it
fires a `JinjaLogWarning` that points the user at the `execute`
docs. Execute-mode callers are unaffected, since a missing name
there is a real user bug (typo on the name, or calling
`load_result` on a name never wrapped in a `statement`).

The warning uses the existing `fire_event(JinjaLogWarning(msg=...))`
pattern already used in `store_seed_results` a few hundred lines
below, so no new imports are needed.

Adds a changelog entry under `.changes/unreleased/` describing the
new warning.

No behaviour change for existing execute-mode calls. Smoke-tested
the branch logic with a minimal fake ProviderContext (parse +
unknown name -> warns, execute + unknown name -> no warn).

Fixes dbt-labs#11070
@SarthakB11
SarthakB11 force-pushed the feat/11070-warn-load-result-parse-phase branch from 369b0f1 to e64494d Compare April 13, 2026 18:54
@cla-bot cla-bot Bot added the cla:yes label Apr 13, 2026
@SarthakB11

Copy link
Copy Markdown
Author

@QMalcolm — would you be the right person to review here? It's a small change in core/dbt/context/providers.py, which you most recently touched. CLA signed, CI green. Happy to iterate on feedback.

@codecov

codecov Bot commented May 6, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.26%. Comparing base (9467792) to head (e64494d).
⚠️ Report is 29 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12815      +/-   ##
==========================================
- Coverage   91.29%   91.26%   -0.03%     
==========================================
  Files         203      203              
  Lines       25916    25919       +3     
==========================================
- Hits        23659    23654       -5     
- Misses       2257     2265       +8     
Flag Coverage Δ
integration 88.06% <100.00%> (-0.11%) ⬇️
unit 65.44% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Unit Tests 65.44% <0.00%> (-0.01%) ⬇️
Integration Tests 88.06% <100.00%> (-0.11%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@QMalcolm QMalcolm left a comment

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.

Hi @SarthakB11, thank you for doing this work ❤️ The changes in providers.py looks good to me.

The one thing we should add before merging is a test. We have a utility, EventCatcher, which is useful for testing changes like this. Here is an example test: https://github.com/dbt-labs/dbt-core/blob/c02340d4c14df1459c00cf91b9ab738e1c4c9507/tests/functional/cli/test_requires.py#L55

@QMalcolm

QMalcolm commented May 6, 2026

Copy link
Copy Markdown
Contributor

@SarthakB11

Copy link
Copy Markdown
Author

Pushed 347a002 with the test using EventCatcher per your suggestion. Both positive (unguarded load_result during parse → warning fires) and negative (guarded with {% if execute %} → silent) cases, matching the tests/functional/cli/test_requires.py:55 pattern.

Quick note on scope: the providers.py fix catches run_query indirectly, since the run_query macro calls load_result to retrieve the stored result. The warning text already names both functions. Happy to add a separate run_query-specific path or a macro-driven test if you'd prefer that to be explicit, but kept this PR focused on what we already aligned on.

@SarthakB11

Copy link
Copy Markdown
Author

Closing this — dbt-core's main branch is now the Rust rewrite (dbt Core v2.0), and core/dbt/context/providers.py (what this PR touches) no longer exists there. The Python v1 codebase moved to main_backup, which was frozen June 1 and isn't taking further merges, and CONTRIBUTING.md now directs all new community PRs to the Rust main. Thanks for the review earlier, @QMalcolm — the underlying gap (silent None from load_result during parse) may still be worth a look if it has a Rust-side equivalent, but that'd need a fresh PR against the current codebase rather than a rebase of this one.

@SarthakB11 SarthakB11 closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla:yes source:community Reported by an external community member (open-source user, not a paying customer).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[logging] include reference to execute mode check when introspective queries like run_query are skipped

2 participants