diff --git a/.changes/unreleased/Features-20250925-171652.yaml b/.changes/unreleased/Features-20250925-171652.yaml new file mode 100644 index 00000000000..ed433d95574 --- /dev/null +++ b/.changes/unreleased/Features-20250925-171652.yaml @@ -0,0 +1,6 @@ +kind: Features +body: 'Validate {{ config }} in SQL for models that don''t statically parse ' +time: 2025-09-25T17:16:52.76822-04:00 +custom: + Author: michelleark + Issue: "12046" diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py index b1ade3cf4b4..ecb2581f80b 100644 --- a/core/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -463,10 +463,14 @@ def config_dict( self._mangle_hooks(config_dict) return config_dict - def render_update(self, node: FinalNode, config: ContextConfig) -> None: + def render_update( + self, node: FinalNode, config: ContextConfig, validate_config_call_dict: bool = False + ) -> None: try: context = self.render_with_context(node, config) - self.update_parsed_node_config(node, config, context=context) + self.update_parsed_node_config( + node, config, context=context, validate_config_call_dict=validate_config_call_dict + ) except ValidationError as exc: # we got a ValidationError - probably bad types in config() raise ConfigUpdateError(exc, node=node) from exc diff --git a/core/dbt/parser/models.py b/core/dbt/parser/models.py index 0dd003d25c0..c4fdf9d0a96 100644 --- a/core/dbt/parser/models.py +++ b/core/dbt/parser/models.py @@ -233,7 +233,9 @@ def parse_python_model(self, node, config, context): config_keys_defaults=config_keys_defaults, ) - def render_update(self, node: ModelNode, config: ContextConfig) -> None: + def render_update( + self, node: ModelNode, config: ContextConfig, validate_config_call_dict: bool = False + ) -> None: self.manifest._parsing_info.static_analysis_path_count += 1 flags = get_flags() if node.language == ModelLanguage.python: @@ -348,7 +350,7 @@ def render_update(self, node: ModelNode, config: ContextConfig) -> None: # if the static parser didn't succeed, fall back to jinja else: # jinja rendering - super().render_update(node, config) + super().render_update(node, config, validate_config_call_dict=True) # if sampling, add the correct messages for tracking if exp_sample and isinstance(experimental_sample, str): diff --git a/tests/functional/deprecations/fixtures.py b/tests/functional/deprecations/fixtures.py index 1707d8950fe..6c9251480ff 100644 --- a/tests/functional/deprecations/fixtures.py +++ b/tests/functional/deprecations/fixtures.py @@ -10,6 +10,17 @@ select 1 as id """ +models_custom_key_in_config_sql = """ +{{ config(my_custom_key="my_custom_value") }} +select 1 as id +""" + +models_custom_key_in_config_non_static_parser_sql = """ +{{ config(my_custom_key="my_custom_value") }} + +select {{ dbt.current_timestamp() }} as my_timestamp +""" + macros__custom_test_sql = """ {% test custom(model) %} select * from {{ model }} diff --git a/tests/functional/deprecations/test_deprecations.py b/tests/functional/deprecations/test_deprecations.py index 860fc30d7af..5f33cd8c794 100644 --- a/tests/functional/deprecations/test_deprecations.py +++ b/tests/functional/deprecations/test_deprecations.py @@ -38,6 +38,8 @@ deprecated_model_exposure_yaml, duplicate_keys_yaml, invalid_deprecation_date_yaml, + models_custom_key_in_config_non_static_parser_sql, + models_custom_key_in_config_sql, models_trivial__model_sql, multiple_custom_keys_in_config_yaml, property_moved_to_config_yaml, @@ -374,6 +376,36 @@ def test_custom_key_in_config_deprecation(self, project): ) +class TestCustomKeyInConfigSQLDeprecation: + @pytest.fixture(scope="class") + def models(self): + return { + "model_custom_key_in_config.sql": models_custom_key_in_config_sql, + } + + @mock.patch("dbt.jsonschemas._JSONSCHEMA_SUPPORTED_ADAPTERS", {"postgres"}) + @mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"}) + def test_custom_key_in_config_sql_deprecation(self, project): + event_catcher = EventCatcher(CustomKeyInConfigDeprecation) + run_dbt( + ["parse", "--no-partial-parse", "--show-all-deprecations"], + callbacks=[event_catcher.catch], + ) + assert len(event_catcher.caught_events) == 1 + assert ( + "Custom key `my_custom_key` found in `config`" + in event_catcher.caught_events[0].info.msg + ) + + +class TestCustomKeyInConfigComplexSQLDeprecation(TestCustomKeyInConfigSQLDeprecation): + @pytest.fixture(scope="class") + def models(self): + return { + "model_custom_key_in_config.sql": models_custom_key_in_config_non_static_parser_sql, + } + + class TestMultipleCustomKeysInConfigDeprecation: @pytest.fixture(scope="class") def models(self):