dbt-labs / dbt-labs/dbt

[1.12.3 Bug] `statically_parse_unrendered_config` stringifies list/dict config values, causing false positives in `upgrade_manifest_json_dbt_version` / `state:modified`

Open
#16,133 4 comments 0 reactions 0 assignees View on GitHub
bug engine:v1 status:triage triage type:bug
Dominant language
Rust
Stars
13.8k
Forks
2.6k
Avg merge
21h 31m
Merged PRs (30d)
56

Description

### Is this a new bug in dbt-core?

- [x] I believe this is a new bug in dbt-core
- [x] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

When loading a `--state` manifest whose `metadata.dbt_version` differs from the
running dbt's version, `upgrade_manifest_json_dbt_version`
(`core/dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py`) re-derives each
node's `unrendered_config` by re-parsing its raw SQL's `{{ config(...) }}` call via
`statically_parse_unrendered_config` (`core/dbt/clients/jinja_static.py`) — gated by
`state_modified_compare_more_unrendered_values` (default `true` since 1.12).

That parser's `construct_static_kwarg_value` is typed `-> str` and stringifies
list/dict values unconditionally. `{{ config(cluster_by=['id']) }}` ends up with
`unrendered_config['cluster_by'] == "['id']"` instead of the real list `['id']`.

Since `same_config`/`same_contents` compare `unrendered_config` with plain `==`, this
type mismatch makes `state:modified` falsely flag the model as changed — even though
`same_body`/checksum (the actual SQL) is identical.

Affects any model with a list/dict-valued config key declared inline (`cluster_by`,
`partition_by`, `grants`, `labels`, `meta`, `unique_key` as a list, etc.) whenever
`state:modified` compares manifests from two different dbt-core versions/builds.

### Expected Behavior

The static parser should preserve the real type of list/dict config values (or at
least produce something that round-trips via `ast.literal_eval`), so the upgrade merge
doesn't introduce a type mismatch against a freshly-parsed, unchanged model.

### Steps To Reproduce

1. `{{ config(cluster_by=['id']) }}` in a model's SQL.
2. `dbt parse`, keep the manifest (the "new" side).
3. Copy it, edit `metadata.dbt_version` to a different version string (the "old" side).
4. `dbt ls --select state:modified --state `

**Expected:** model not selected. **Actual:** selected — `unrendered_config['cluster_by']`
was rewritten from `['id']` to `"['id']"` by the upgrade step.

### Relevant log output

```shell
$ dbt ls --select state:modified --state ./old-manifest/
model.my_project.my_model

(despite identical `raw_code`/checksum on both sides)
```

### Environment

```markdown
- OS: macOS
- Python: 3.13.0
- dbt: 1.12.3 (also present in 1.12.0)
```

### Which database adapter are you using with dbt?

bigquery

### Additional Context

Root-caused against a real ~450-model project: calling `same_body`, `same_config`,
`same_persisted_description`, `same_fqn`, `same_database_representation`,
`same_contract`, `same_ref_representation` directly (via `dbt.contracts.state.PreviousState`)
reproduced the flagged/not-flagged status of every model exactly (366/366).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.