Skip to content

Commit b0b9004

Browse files
authored
Support for declaraing variables in a vars.yml file (#12488)
1 parent b09f1ee commit b0b9004

10 files changed

Lines changed: 792 additions & 22 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Features
2+
body: Added support for vars.yml to declare project variables
3+
time: 2026-02-12T23:27:28.294557+05:30
4+
custom:
5+
Author: sriramr98
6+
Issue: 11144 2955

core/dbt/config/project.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
PACKAGE_LOCK_FILE_NAME,
2222
PACKAGE_LOCK_HASH_KEY,
2323
PACKAGES_FILE_NAME,
24+
VARS_FILE_NAME,
2425
)
2526
from dbt.contracts.project import PackageConfig
2627
from dbt.contracts.project import Project as ProjectContract
@@ -108,6 +109,31 @@ def load_yml_dict(file_path):
108109
return ret
109110

110111

112+
def vars_data_from_root(project_root: str) -> Dict[str, Any]:
113+
"""Load vars from vars.yml file if it exists.
114+
115+
Returns the contents of the 'vars' key, or empty dict if file doesn't exist or has no vars key.
116+
"""
117+
vars_yml_path = os.path.join(project_root, VARS_FILE_NAME)
118+
vars_file_dict = load_yml_dict(vars_yml_path)
119+
if not vars_file_dict:
120+
return {}
121+
return vars_file_dict.get("vars", {})
122+
123+
124+
def validate_vars_not_in_both(
125+
project_dict: Dict[str, Any],
126+
has_vars_file: bool,
127+
) -> None:
128+
"""Raise error if vars defined in both vars.yml and dbt_project.yml."""
129+
has_project_vars = "vars" in project_dict and project_dict["vars"]
130+
131+
if has_vars_file and has_project_vars:
132+
raise DbtProjectError(
133+
f"Variables cannot be defined in both {VARS_FILE_NAME} and {DBT_PROJECT_FILE_NAME}. "
134+
)
135+
136+
111137
def package_and_project_data_from_root(project_root):
112138
packages_yml_dict = load_yml_dict(f"{project_root}/{PACKAGES_FILE_NAME}")
113139
dependencies_yml_dict = load_yml_dict(f"{project_root}/{DEPENDENCIES_FILE_NAME}")
@@ -338,10 +364,14 @@ def get_rendered(
338364
)
339365

340366
# Called by Project.from_project_root which first calls PartialProject.from_project_root
341-
def render(self, renderer: DbtProjectYamlRenderer) -> "Project":
367+
def render(
368+
self,
369+
renderer: DbtProjectYamlRenderer,
370+
vars_from_file: Optional[Dict[str, Any]] = None,
371+
) -> "Project":
342372
try:
343373
rendered = self.get_rendered(renderer)
344-
return self.create_project(rendered)
374+
return self.create_project(rendered, vars_from_file=vars_from_file)
345375
except DbtProjectError as exc:
346376
if exc.path is None:
347377
exc.path = os.path.join(self.project_root, DBT_PROJECT_FILE_NAME)
@@ -376,7 +406,11 @@ def check_config_path(
376406
kwargs.update({"exp_path": expected_path})
377407
deprecations.warn(f"project-config-{deprecated_path}", **kwargs)
378408

379-
def create_project(self, rendered: RenderComponents) -> "Project":
409+
def create_project(
410+
self,
411+
rendered: RenderComponents,
412+
vars_from_file: Optional[Dict[str, Any]] = None,
413+
) -> "Project":
380414
unrendered = RenderComponents(
381415
project_dict=self.project_dict,
382416
packages_dict=self.packages_dict,
@@ -485,10 +519,13 @@ def create_project(self, rendered: RenderComponents) -> "Project":
485519
saved_queries = cfg.saved_queries
486520
exposures = cfg.exposures
487521
functions = cfg.functions
488-
if cfg.vars is None:
489-
vars_dict: Dict[str, Any] = {}
522+
523+
# Use vars from vars.yml if provided, otherwise use vars from dbt_project.yml
524+
# Mutual exclusivity ensures only one source is populated
525+
if vars_from_file:
526+
vars_dict = vars_from_file
490527
else:
491-
vars_dict = cfg.vars
528+
vars_dict = cfg.vars or {}
492529

493530
vars_value = VarProvider(vars_dict)
494531
# There will never be any project_env_vars when it's first created
@@ -557,6 +594,7 @@ def create_project(self, rendered: RenderComponents) -> "Project":
557594
restrict_access=cfg.restrict_access,
558595
dbt_cloud=dbt_cloud,
559596
flags=flags,
597+
vars_from_file=vars_from_file or {},
560598
)
561599
# sanity check - this means an internal issue
562600
project.validate()
@@ -676,6 +714,7 @@ class Project:
676714
restrict_access: bool
677715
dbt_cloud: Dict[str, Any]
678716
flags: Dict[str, Any]
717+
vars_from_file: Dict[str, Any]
679718

680719
@property
681720
def all_source_paths(self) -> List[str]:
@@ -786,11 +825,16 @@ def from_project_root(
786825
*,
787826
verify_version: bool = False,
788827
validate: bool = False,
828+
vars_from_file: Optional[Dict[str, Any]] = None,
789829
) -> "Project":
790830
partial = PartialProject.from_project_root(
791831
project_root, verify_version=verify_version, validate=validate
792832
)
793-
return partial.render(renderer)
833+
834+
# Check mutual exclusivity before rendering
835+
validate_vars_not_in_both(partial.project_dict, bool(vars_from_file))
836+
837+
return partial.render(renderer, vars_from_file=vars_from_file)
794838

795839
def hashed_name(self):
796840
return md5(self.project_name)

core/dbt/config/runtime.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
from dbt.adapters.contracts.relation import ComponentName
2525
from dbt.adapters.factory import get_include_paths, get_relation_class_by_name
2626
from dbt.artifacts.resources import Quoting
27-
from dbt.config.project import load_package_lock_config, load_raw_project
27+
from dbt.config.project import (
28+
load_package_lock_config,
29+
load_raw_project,
30+
vars_data_from_root,
31+
)
2832
from dbt.contracts.graph.manifest import ManifestMetadata
2933
from dbt.contracts.project import Configuration
3034
from dbt.events.types import UnusedResourceConfigPath
@@ -54,10 +58,24 @@ def load_project(
5458
validate: bool = False,
5559
require_vars: bool = True,
5660
) -> Project:
57-
# get the project with all of the provided information
58-
project_renderer = DbtProjectYamlRenderer(profile, cli_vars, require_vars=require_vars)
61+
62+
if cli_vars is None:
63+
cli_vars = {}
64+
65+
# Load vars.yml first (before rendering dbt_project.yml)
66+
vars_from_file = vars_data_from_root(project_root)
67+
68+
# Merge: CLI vars take precedence over file vars
69+
merged_vars = {**vars_from_file, **cli_vars}
70+
71+
# Renderer receives merged vars for Jinja in dbt_project.yml
72+
project_renderer = DbtProjectYamlRenderer(profile, merged_vars, require_vars=require_vars)
5973
project = Project.from_project_root(
60-
project_root, project_renderer, verify_version=version_check, validate=validate
74+
project_root,
75+
project_renderer,
76+
verify_version=version_check,
77+
validate=validate,
78+
vars_from_file=vars_from_file,
6179
)
6280

6381
# Save env_vars encountered in rendering for partial parsing
@@ -199,6 +217,7 @@ def from_parts(
199217
dependencies=dependencies,
200218
dbt_cloud=project.dbt_cloud,
201219
flags=project.flags,
220+
vars_from_file=project.vars_from_file,
202221
)
203222

204223
# Called by 'load_projects' in this class

core/dbt/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313

1414
DBT_PROJECT_FILE_NAME = "dbt_project.yml"
15+
VARS_FILE_NAME = "vars.yml"
1516
PACKAGES_FILE_NAME = "packages.yml"
1617
DEPENDENCIES_FILE_NAME = "dependencies.yml"
1718
PACKAGE_LOCK_FILE_NAME = "package-lock.yml"

core/dbt/parser/manifest.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,16 +1030,18 @@ def build_manifest_state_check(self):
10301030
v for k, v in config.cli_vars.items() if k.startswith(SECRET_ENV_PREFIX) and v.strip()
10311031
]
10321032
stringified_cli_vars = pprint.pformat(config.cli_vars)
1033-
vars_hash = FileHash.from_contents(
1034-
"\x00".join(
1035-
[
1036-
stringified_cli_vars,
1037-
config.profile_name,
1038-
config.target_name,
1039-
__version__,
1040-
]
1041-
)
1042-
)
1033+
vars_hash_contents = [
1034+
stringified_cli_vars,
1035+
config.profile_name,
1036+
config.target_name,
1037+
__version__,
1038+
]
1039+
1040+
# We only add vars from `vars.yml` if it's present to prevent affecting users not using vars.yml from getting fully parsed.
1041+
if config.vars_from_file:
1042+
vars_hash_contents.append(pprint.pformat(config.vars_from_file))
1043+
1044+
vars_hash = FileHash.from_contents("\x00".join(vars_hash_contents))
10431045
fire_event(
10441046
StateCheckVarsHash(
10451047
checksum=vars_hash.checksum,

core/dbt/tests/fixtures/project.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def project_config_update():
191191
# Combines the project_config_update dictionary with project_config defaults to
192192
# produce a project_yml config and write it out as dbt_project.yml
193193
@pytest.fixture(scope="class")
194-
def dbt_project_yml(project_root, project_config_update):
194+
def dbt_project_yml(project_root, project_config_update, vars_yml):
195195
project_config = {
196196
"name": "test",
197197
"profile": "test",
@@ -259,6 +259,23 @@ def selectors_yml(project_root, selectors):
259259
write_file(data, project_root, "selectors.yml")
260260

261261

262+
# Fixture to provide vars as either yaml or dictionary
263+
@pytest.fixture(scope="class")
264+
def vars_yml_update():
265+
return {}
266+
267+
268+
# Write out the vars.yml file
269+
@pytest.fixture(scope="class")
270+
def vars_yml(project_root, vars_yml_update):
271+
if vars_yml_update:
272+
if isinstance(vars_yml_update, str):
273+
data = vars_yml_update
274+
else:
275+
data = yaml.safe_dump(vars_yml_update)
276+
write_file(data, project_root, "vars.yml")
277+
278+
262279
# This fixture ensures that the logging infrastructure does not accidentally
263280
# reuse streams configured on previous test runs, which might now be closed.
264281
# It should be run before (and so included as a parameter by) any other fixture
@@ -407,6 +424,7 @@ def project_files(
407424
selectors_yml,
408425
dependencies_yml,
409426
packages_yml,
427+
vars_yml,
410428
dbt_project_yml,
411429
):
412430
write_project_files(project_root, "models", {**models, **properties})

0 commit comments

Comments
 (0)