diff --git a/src/sqlbuild/compiler/compile/_helpers/attachment/sources.py b/src/sqlbuild/compiler/compile/_helpers/attachment/sources.py index fd6a37dd64..3383ed4e17 100644 --- a/src/sqlbuild/compiler/compile/_helpers/attachment/sources.py +++ b/src/sqlbuild/compiler/compile/_helpers/attachment/sources.py @@ -61,6 +61,7 @@ def build_source_inputs( source_inputs: list[CompileSourceInput] = [] sql_validation_enabled: bool = effective_settings.sql_analysis and not no_sql_validation resolver: DeclarationScopeResolver | None = declaration_expansion.resolver + default_contract: str | None = discovered_inputs.project_config.defaults.contract reuse_file_scope: bool = ( resolver is not None and not any( @@ -76,6 +77,12 @@ def build_source_inputs( for source_file in discovered_inputs.source_files: source_entry: SourceEntry for raw_source_entry in source_file.source_entries: + effective_source_entry: SourceEntry = raw_source_entry + if raw_source_entry.contract is None and default_contract is not None: + effective_source_entry = replace( + raw_source_entry, + contract=default_contract, + ) source_resource: ResourceIdentity = ResourceIdentity( ResourceKind.SOURCE, raw_source_entry.name ) @@ -95,7 +102,7 @@ def build_source_inputs( if reuse_file_scope: declarations_by_file[source_file.file_path] = scoped_declarations source_entry, usages = expand_source_entry_templates( - source_entry=raw_source_entry, + source_entry=effective_source_entry, file_path=source_file.file_path, effective_vars=effective_vars, loaded_macros=loaded_macros, diff --git a/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py b/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py index d7fcf3e1be..3a6a977d57 100644 --- a/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py +++ b/tests/integration/src/sqlbuild/cli/commands/main/test_rules.py @@ -270,14 +270,14 @@ def test_given_contract_rules_when_compiling_then_contract_is_enforced_typed_and ) -> None: (tmp_path / "sqlbuild_project.toml").write_text( 'name = "orders"\nadapter = "duckdb"\n\n[rules]\n' - 'select = ["SQBRCONTRACT101", "SQBRCONTRACT105", "SQBRCONTRACT106"]\n', + 'select = ["SQBRCONTRACT101", "SQBRCONTRACT105", "SQBRCONTRACT106"]\n\n' + '[defaults]\ncontract = "enforced"\n', encoding="utf-8", ) model: Path = tmp_path / "models" / "orders.sql" model.parent.mkdir() model.write_text( "MODEL (\n" - " contract enforced,\n" f" columns ({test_case.columns_sql}),\n" ");\n\n" "SELECT CAST(1 AS INTEGER) AS order_id, CAST('ready' AS VARCHAR) AS status\n", diff --git a/tests/integration/src/sqlbuild/compiler/pipeline/_test_types.py b/tests/integration/src/sqlbuild/compiler/pipeline/_test_types.py index 559369b187..8a5cdd7165 100644 --- a/tests/integration/src/sqlbuild/compiler/pipeline/_test_types.py +++ b/tests/integration/src/sqlbuild/compiler/pipeline/_test_types.py @@ -49,6 +49,15 @@ class SemanticBindingIntegrationTestCase: expected_exit_code: int +@dataclass(frozen=True) +class SourceContractDefaultIntegrationTestCase: + description: str + source_contract_yaml: str + expected_exit_code: int + expected_output_fragment: str + expected_absent_output_fragment: str + + @dataclass(frozen=True) class ExpectedModelEntry: description: str diff --git a/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_binding.py b/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_binding.py index e2d1770cd8..24cbc0e024 100644 --- a/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_binding.py +++ b/tests/integration/src/sqlbuild/compiler/pipeline/test_semantic_binding.py @@ -12,6 +12,7 @@ from tests.integration.src.sqlbuild.compiler.pipeline._test_types import ( SemanticBindingClauseIntegrationTestCase, SemanticBindingIntegrationTestCase, + SourceContractDefaultIntegrationTestCase, ) from tests.integration.src.sqlbuild.compiler.pipeline.helpers import write_semantic_binding_project @@ -198,39 +199,50 @@ def test_given_partial_upstream_when_column_absence_is_unproven_then_compile_suc @pytest.mark.parametrize( "test_case", [ - SemanticBindingIntegrationTestCase( - description="given enforced source contract when column is missing then compile fails", + SourceContractDefaultIntegrationTestCase( + description="given default enforced source contract when column is missing then compile fails", + source_contract_yaml="", expected_exit_code=1, - ) + expected_output_fragment="error[B002]: Unknown column 'missing' in table 'raw_orders'", + expected_absent_output_fragment="0 errors, 0 warnings", + ), + SourceContractDefaultIntegrationTestCase( + description="given explicit open source contract when default is enforced then compile succeeds", + source_contract_yaml=" contract: none\n", + expected_exit_code=0, + expected_output_fragment="0 errors, 0 warnings", + expected_absent_output_fragment="error[B002]", + ), ], ids=lambda case: case.description, ) -def test_given_enforced_source_contract_when_column_is_missing_then_compile_fails( - test_case: SemanticBindingIntegrationTestCase, +def test_given_source_contract_default_when_compiling_then_effective_policy_controls_authority( + test_case: SourceContractDefaultIntegrationTestCase, tmp_path: Path, capsys: pytest.CaptureFixture[str], ) -> None: _ = (tmp_path / "sqlbuild_project.toml").write_text( - 'name = "semantic_binding"\nadapter = "duckdb"\n', encoding="utf-8" + 'name = "semantic_binding"\nadapter = "duckdb"\n\n[defaults]\ncontract = "enforced"\n', + encoding="utf-8", ) sources_dir: Path = tmp_path / "sources" models_dir: Path = tmp_path / "models" sources_dir.mkdir() models_dir.mkdir() _ = (sources_dir / "raw.yml").write_text( - """sources: + f"""sources: - name: raw_orders schema: raw table: orders - contract: enforced - columns: +{test_case.source_contract_yaml} columns: - name: id type: INTEGER """, encoding="utf-8", ) _ = (models_dir / "downstream.sql").write_text( - 'MODEL (materialized view);\nSELECT missing FROM __source("raw_orders")\n', + "MODEL (materialized view, columns (missing (type INTEGER)));\n" + 'SELECT CAST(missing AS INTEGER) AS missing FROM __source("raw_orders")\n', encoding="utf-8", ) @@ -238,7 +250,8 @@ def test_given_enforced_source_contract_when_column_is_missing_then_compile_fail output: str = capsys.readouterr().out assert exit_code == test_case.expected_exit_code - assert "error[B002]: Unknown column 'missing' in table 'raw_orders'" in output + assert test_case.expected_output_fragment in output + assert test_case.expected_absent_output_fragment not in output @pytest.mark.parametrize(