Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions sqlmesh/dbt/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
94 changes: 93 additions & 1 deletion tests/dbt/test_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 5 additions & 4 deletions tests/fixtures/dbt/sushi_test/models/top_waiters.sql
Original file line number Diff line number Diff line change
@@ -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 %}
Expand Down