diff --git a/sqlmesh/dbt/manifest.py b/sqlmesh/dbt/manifest.py index ea2058138f..e058eb66a8 100644 --- a/sqlmesh/dbt/manifest.py +++ b/sqlmesh/dbt/manifest.py @@ -390,6 +390,12 @@ def _load_models_and_seeds(self) -> None: if node_version: node_name = f"{node_name}_v{node_version}" + model_kwargs = node_config.copy() + if not model_kwargs.get("quoting") and ( + quoting := getattr(self._manifest.metadata, "quoting", None) + ): + model_kwargs["quoting"] = quoting.to_dict() + if node.resource_type in {"model", "snapshot"}: sql = node.raw_code if DBT_VERSION >= (1, 3, 0) else node.raw_sql # type: ignore dependencies = Dependencies( @@ -408,22 +414,12 @@ def _load_models_and_seeds(self) -> None: self._flatten_dependencies_from_macros(dependencies.macros, node.package_name) ) - self._models_per_package[node.package_name][node_name] = ModelConfig( - **dict( - node_config, - sql=sql, - dependencies=dependencies, - tests=tests, - ) - ) + model_kwargs.update({"sql": sql, "dependencies": dependencies, "tests": tests}) + self._models_per_package[node.package_name][node_name] = ModelConfig(**model_kwargs) else: - self._seeds_per_package[node.package_name][node_name] = SeedConfig( - **dict( - node_config, - dependencies=Dependencies(macros=macro_references), - tests=tests, - ) - ) + dependencies = Dependencies(macros=macro_references) + model_kwargs.update({"dependencies": dependencies, "tests": tests}) + self._seeds_per_package[node.package_name][node_name] = SeedConfig(**model_kwargs) def _load_on_run_start_end(self) -> None: for node in self._manifest.nodes.values(): diff --git a/tests/.gitignore b/tests/.gitignore index fff33c1c74..bca17f9256 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1 +1,2 @@ -sqlmesh_pyproject.toml \ No newline at end of file +sqlmesh_pyproject.toml +.user.yml diff --git a/tests/dbt/test_config.py b/tests/dbt/test_config.py index 5dccd90ed2..be7022decc 100644 --- a/tests/dbt/test_config.py +++ b/tests/dbt/test_config.py @@ -477,14 +477,14 @@ def test_seed_config(sushi_test_project: Project, mocker: MockerFixture): assert actual_config == expected_config context = sushi_test_project.context - assert raw_items_seed.canonical_name(context) == "sushi.waiter_names" - assert raw_items_seed.to_sqlmesh(context).name == "sushi.waiter_names" + assert raw_items_seed.canonical_name(context) == '"sushi"."waiter_names"' + assert raw_items_seed.to_sqlmesh(context).name == '"sushi"."waiter_names"' raw_items_seed.dialect_ = "snowflake" - assert raw_items_seed.to_sqlmesh(sushi_test_project.context).name == "sushi.waiter_names" + assert raw_items_seed.to_sqlmesh(sushi_test_project.context).name == '"sushi"."waiter_names"' assert ( raw_items_seed.to_sqlmesh(sushi_test_project.context).fqn - == '"MEMORY"."SUSHI"."WAITER_NAMES"' + == '"MEMORY"."sushi"."waiter_names"' ) waiter_revenue_semicolon_seed = seed_configs["waiter_revenue_semicolon"] @@ -499,9 +499,13 @@ def test_seed_config(sushi_test_project: Project, mocker: MockerFixture): } assert actual_config_semicolon == expected_config_semicolon - assert waiter_revenue_semicolon_seed.canonical_name(context) == "sushi.waiter_revenue_semicolon" assert ( - waiter_revenue_semicolon_seed.to_sqlmesh(context).name == "sushi.waiter_revenue_semicolon" + waiter_revenue_semicolon_seed.canonical_name(context) + == '"sushi"."waiter_revenue_semicolon"' + ) + assert ( + waiter_revenue_semicolon_seed.to_sqlmesh(context).name + == '"sushi"."waiter_revenue_semicolon"' ) assert waiter_revenue_semicolon_seed.delimiter == ";" assert set(waiter_revenue_semicolon_seed.columns.keys()) == {"waiter_id", "revenue", "quarter"} diff --git a/tests/dbt/test_manifest.py b/tests/dbt/test_manifest.py index 2ecf8b8980..9b98386254 100644 --- a/tests/dbt/test_manifest.py +++ b/tests/dbt/test_manifest.py @@ -367,3 +367,83 @@ def test_macro_assignment_shadowing(create_empty_project): models = helper.models() assert "model_using_path_macro" in models assert "path" in models["model_using_path_macro"].dependencies.model_attrs.attrs + + +def test_quoting_config(tmp_path: Path): + if DBT_VERSION < (1, 10, 0): + pytest.skip( + "The 'quoting' setting in dbt_projects.yml is not respected for dbt-core < v1.10" + ) + + # Create dbt_project.yml with quoting config + (tmp_path / "dbt_project.yml").write_text(""" +name: 'test_project' +version: '1.0.0' +config-version: 2 +profile: 'test_project' + +model-paths: ["models"] + +models: + test_project: + +materialized: table + +quoting: + database: true + schema: true + identifier: false +""") + + # Create profiles.yml + (tmp_path / "profiles.yml").write_text(""" +test_project: + target: dev + outputs: + dev: + type: duckdb + path: ':memory:' +""") + + # Create a simple model without quoting override + models_dir = tmp_path / "models" + models_dir.mkdir() + (models_dir / "test_model.sql").write_text("SELECT 1 as id") + + # Create a model with inline quoting override + (models_dir / "test_model_with_override.sql").write_text(""" +{{ + config( + quoting={ + "database": false, + "schema": false, + "identifier": true + } + ) +}} +SELECT 2 as id +""") + + profile = Profile.load(DbtContext(tmp_path)) + helper = ManifestHelper( + tmp_path, + tmp_path, + "test_project", + profile.target, + model_defaults=ModelDefaultsConfig(start="2020-01-01"), + ) + + models = helper.models() + test_model = models["test_model"] + + # Model should inherit quoting from dbt_project.yml + assert test_model.quoting is not None + assert test_model.quoting["database"] is True + assert test_model.quoting["schema"] is True + assert test_model.quoting["identifier"] is False + + # Model with inline override should use its own quoting settings + test_model_override = models["test_model_with_override"] + assert test_model_override.quoting is not None + assert test_model_override.quoting["database"] is False + assert test_model_override.quoting["schema"] is False + assert test_model_override.quoting["identifier"] is True diff --git a/tests/dbt/test_model.py b/tests/dbt/test_model.py index 797d638858..efc47cb9f9 100644 --- a/tests/dbt/test_model.py +++ b/tests/dbt/test_model.py @@ -556,7 +556,7 @@ def test_load_deprecated_incremental_time_column( assert model.kind.auto_restatement_intervals is None assert model.kind.partition_by_time_column is True assert ( - "Using `time_column` on a model with incremental_strategy 'delete+insert' has been deprecated. Please use `incremental_by_time_range` instead in model 'main.incremental_time_range'." + "Using `time_column` on a model with incremental_strategy 'delete+insert' has been deprecated. Please use `incremental_by_time_range` instead in model '\"main\".\"incremental_time_range\"'." in caplog.text ) diff --git a/tests/dbt/test_transformation.py b/tests/dbt/test_transformation.py index 97c5c37e75..3ab2b7bb03 100644 --- a/tests/dbt/test_transformation.py +++ b/tests/dbt/test_transformation.py @@ -164,7 +164,7 @@ def test_dbt_custom_materialization_with_time_filter_and_macro(): today = datetime.now() # select both custom materialiasation models with the wildcard - selector = ["sushi.custom_incremental*"] + selector = ['"sushi"."custom_incremental*'] plan_builder = sushi_context.plan_builder(select_models=selector, execution_time=today) plan = plan_builder.build() @@ -191,6 +191,7 @@ def test_dbt_custom_materialization_with_time_filter_and_macro(): # - run ONE DAY LATER a_day_later = today + timedelta(days=1) + selector = ['"sushi"."custom_incremental*'] sushi_context.run(select_models=selector, execution_time=a_day_later) result_after_run = sushi_context.engine_adapter.fetchdf(select_daily) @@ -2712,7 +2713,7 @@ def test_selected_resources_with_selectors(): assert any("customers" in model for model in plan.selected_models) # Test wildcard selection - plan_builder = sushi_context.plan_builder(select_models=["sushi.waiter_*"]) + plan_builder = sushi_context.plan_builder(select_models=['"sushi"."waiter_*']) plan = plan_builder.build() assert plan.selected_models is not None assert len(plan.selected_models) >= 4