diff --git a/sqlmesh/dbt/builtin.py b/sqlmesh/dbt/builtin.py index 0a2d837c28..c4fe0540b7 100644 --- a/sqlmesh/dbt/builtin.py +++ b/sqlmesh/dbt/builtin.py @@ -170,8 +170,24 @@ class Config: def __init__(self, config_dict: t.Dict[str, t.Any]) -> None: self._config = config_dict - def __call__(self, **kwargs: t.Any) -> str: - self._config.update(**kwargs) + def __call__(self, *args: t.Any, **kwargs: t.Any) -> str: + if args and kwargs: + raise ConfigError( + "Invalid inline model config: cannot mix positional and keyword arguments" + ) + + if args: + if len(args) == 1 and isinstance(args[0], dict): + # Single dict argument: config({"materialized": "table"}) + self._config.update(args[0]) + else: + raise ConfigError( + f"Invalid inline model config: expected a single dictionary, got {len(args)} arguments" + ) + elif kwargs: + # Keyword arguments: config(materialized="table") + self._config.update(kwargs) + return "" def set(self, name: str, value: t.Any) -> str: diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index c8cc688a38..1db3d469d8 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -10,6 +10,7 @@ import pytest from dbt.adapters.base import BaseRelation +from jinja2 import Template if DBT_VERSION >= (1, 4, 0): from dbt.exceptions import CompilationError @@ -42,7 +43,7 @@ OnAdditiveChange, ) from sqlmesh.core.state_sync.db.snapshot import _snapshot_to_json -from sqlmesh.dbt.builtin import _relation_info_to_relation +from sqlmesh.dbt.builtin import _relation_info_to_relation, Config from sqlmesh.dbt.common import Dependencies from sqlmesh.dbt.column import ( ColumnConfig, @@ -1052,6 +1053,97 @@ def test_config_jinja(sushi_test_project: Project): assert model.render_pre_statements()[0].sql() == '"bar"' +@pytest.mark.xdist_group("dbt_manifest") +def test_config_dict_syntax(): + # Test dictionary syntax + config = Config({}) + result = config({"materialized": "table", "alias": "dict_table"}) + assert result == "" + assert config._config["materialized"] == "table" + assert config._config["alias"] == "dict_table" + + # Test kwargs syntax still works + config2 = Config({}) + result = config2(materialized="view", alias="kwargs_table") + assert result == "" + assert config2._config["materialized"] == "view" + assert config2._config["alias"] == "kwargs_table" + + # Test that mixing args and kwargs is rejected + config3 = Config({}) + try: + config3({"materialized": "table"}, alias="mixed") + assert False, "Should have raised ConfigError" + except Exception as e: + assert "cannot mix positional and keyword arguments" in str(e) + + # Test nested dicts + config4 = Config({}) + config4({"meta": {"owner": "data_team", "priority": 1}, "tags": ["daily", "critical"]}) + assert config4._config["meta"]["owner"] == "data_team" + assert config4._config["tags"] == ["daily", "critical"] + + # Test multiple positional arguments are rejected + config4 = Config({}) + try: + config4({"materialized": "table"}, {"alias": "test"}) + assert False + except Exception as e: + assert "expected a single dictionary, got 2 arguments" in str(e) + + +def test_config_dict_in_jinja(): + # Test dict syntax directly with Config class + config = Config({}) + template = Template("{{ config({'materialized': 'table', 'unique_key': 'id'}) }}done") + result = template.render(config=config) + assert result == "done" + assert config._config["materialized"] == "table" + assert config._config["unique_key"] == "id" + + # Test with nested dict and list values + config2 = Config({}) + complex_template = Template("""{{ config({ + 'tags': ['test', 'dict'], + 'meta': {'owner': 'data_team'} + }) }}result""") + result = complex_template.render(config=config2) + assert result == "result" + assert config2._config["tags"] == ["test", "dict"] + assert config2._config["meta"]["owner"] == "data_team" + + # Test that kwargs still work + config3 = Config({}) + kwargs_template = Template("{{ config(materialized='view', alias='my_view') }}done") + result = kwargs_template.render(config=config3) + assert result == "done" + assert config3._config["materialized"] == "view" + assert config3._config["alias"] == "my_view" + + +@pytest.mark.xdist_group("dbt_manifest") +def test_config_dict_syntax_in_sushi_project(sushi_test_project: Project): + assert sushi_test_project is not None + assert sushi_test_project.context is not None + + sushi_package = sushi_test_project.packages.get("sushi") + assert sushi_package is not None + + top_waiters_found = False + for model_config in sushi_package.models.values(): + if model_config.name == "top_waiters": + # top_waiters model now uses dict config syntax with: + # config({'materialized': 'view', 'limit_value': var('top_waiters:limit'), 'meta': {...}}) + top_waiters_found = True + assert model_config.materialized == "view" + assert model_config.meta is not None + assert model_config.meta.get("owner") == "analytics_team" + assert model_config.meta.get("priority") == "high" + break + + assert top_waiters_found + + @pytest.mark.xdist_group("dbt_manifest") def test_config_jinja_get_methods(sushi_test_project: Project): model_config = ModelConfig( diff --git a/tests/fixtures/dbt/sushi_test/models/top_waiters.sql b/tests/fixtures/dbt/sushi_test/models/top_waiters.sql index 265a99eb7a..ce7e2154c5 100644 --- a/tests/fixtures/dbt/sushi_test/models/top_waiters.sql +++ b/tests/fixtures/dbt/sushi_test/models/top_waiters.sql @@ -1,8 +1,9 @@ {{ - config( - materialized='view', - limit_value=var('top_waiters:limit'), - ) + config({ + 'materialized': 'view', + 'limit_value': var('top_waiters:limit'), + 'meta': {'owner': 'analytics_team', 'priority': 'high'} + }) }} {% set columns = model.columns %}