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
9 changes: 8 additions & 1 deletion src/sqlbuild/compiler/compile/_helpers/attachment/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
)
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -198,47 +199,59 @@ 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",
)

exit_code: int = main(["--no-color", "--project-dir", str(tmp_path), "compile", "--no-cache"])

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(
Expand Down
Loading